From cafc5bcf6da0f9c34c506ad6e3894a2e89d4966c Mon Sep 17 00:00:00 2001 From: Kellermorph Date: Sun, 13 Sep 2026 20:36:59 +0200 Subject: [PATCH 01/18] Add Hide linked elements checkbox to PLC link widget --- sources/ui/plclinkwidget.cpp | 86 +++++++++++++++++++++++++----------- sources/ui/plclinkwidget.h | 3 ++ 2 files changed, 62 insertions(+), 27 deletions(-) diff --git a/sources/ui/plclinkwidget.cpp b/sources/ui/plclinkwidget.cpp index 144ee0bff..4949be237 100644 --- a/sources/ui/plclinkwidget.cpp +++ b/sources/ui/plclinkwidget.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -36,6 +37,7 @@ #include #include #include +#include #include #include #include @@ -56,12 +58,16 @@ PlcLinkWidget::PlcLinkWidget(Element *elmt, QWidget *parent) main_layout->addWidget(m_unlink_pb, 0, 1); main_layout->addWidget(m_show_this_pb, 0, 2); - // Row 1: Search field + // Row 1: Hide linked elements checkbox + m_hide_linked_cb = new QCheckBox(tr("Masquer les éléments connectés"), this); + main_layout->addWidget(m_hide_linked_cb, 1, 0, 1, 3); + + // Row 2: Search field m_search_field = new QLineEdit(this); m_search_field->setPlaceholderText(tr("Recherche")); - main_layout->addWidget(m_search_field, 1, 0, 1, 3); + main_layout->addWidget(m_search_field, 2, 0, 1, 3); - // Row 2: Tree widget + // Row 3: Tree widget m_tree_widget = new QTreeWidget(this); m_tree_widget->setHeaderLabels({ tr("Label"), tr("Type"), tr("Adresse"), @@ -78,9 +84,9 @@ PlcLinkWidget::PlcLinkWidget(Element *elmt, QWidget *parent) m_tree_widget->header()->setSectionResizeMode(3, QHeaderView::ResizeToContents); m_tree_widget->header()->setSectionResizeMode(4, QHeaderView::ResizeToContents); m_tree_widget->header()->setSectionResizeMode(5, QHeaderView::ResizeToContents); - main_layout->addWidget(m_tree_widget, 2, 0, 1, 3); + main_layout->addWidget(m_tree_widget, 3, 0, 1, 3); - // Row 3: Hidden masters note + // Row 4: Hidden masters note m_hidden_masters_label = new QLabel( tr("Remarque : les éléments maîtres ayant atteint leur nombre maximal " "d'esclaves sont masqués."), this); @@ -89,9 +95,9 @@ PlcLinkWidget::PlcLinkWidget(Element *elmt, QWidget *parent) italic_font.setItalic(true); m_hidden_masters_label->setFont(italic_font); m_hidden_masters_label->hide(); - main_layout->addWidget(m_hidden_masters_label, 3, 0, 1, 3); + main_layout->addWidget(m_hidden_masters_label, 4, 0, 1, 3); - main_layout->setRowStretch(2, 1); + main_layout->setRowStretch(3, 1); setMinimumWidth(500); @@ -104,6 +110,12 @@ PlcLinkWidget::PlcLinkWidget(Element *elmt, QWidget *parent) this, &PlcLinkWidget::on_m_search_field_textEdited); connect(m_tree_widget, &QTreeWidget::customContextMenuRequested, this, &PlcLinkWidget::on_m_tree_widget_customContextMenuRequested); + connect(m_hide_linked_cb, &QCheckBox::toggled, + this, &PlcLinkWidget::on_m_hide_linked_cb_toggled); + + QSettings settings; + m_hide_linked_cb->setChecked( + settings.value(QStringLiteral("plclinkwidget/hideLinked"), false).toBool()); if (elmt) setElement(elmt); @@ -216,6 +228,7 @@ void PlcLinkWidget::buildPlcTree() parent_item->setExpanded(false); // Add child items for each IO entry + bool all_children_hidden = true; for (int i = 0; i < plc_data.ios.size(); ++i) { const auto &io = plc_data.ios.at(i); auto *child_item = new QTreeWidgetItem(parent_item); @@ -233,33 +246,45 @@ void PlcLinkWidget::buildPlcTree() entry.ioIndex = i; m_io_entry_hash.insert(child_item, entry); - // If this IO is already linked to a slave, grey it out and strike through + // If this IO is already linked to a slave if (used_io_indices.contains(i)) { - QFont strike_font = child_item->font(0); - strike_font.setStrikeOut(true); - child_item->setFont(0, strike_font); - child_item->setFont(1, strike_font); - child_item->setFont(2, strike_font); - child_item->setFont(3, strike_font); - child_item->setFont(4, strike_font); - child_item->setFont(5, strike_font); + if (m_hide_linked_cb->isChecked()) { + child_item->setHidden(true); + } else { + all_children_hidden = false; + QFont strike_font = child_item->font(0); + strike_font.setStrikeOut(true); + child_item->setFont(0, strike_font); + child_item->setFont(1, strike_font); + child_item->setFont(2, strike_font); + child_item->setFont(3, strike_font); + child_item->setFont(4, strike_font); + child_item->setFont(5, strike_font); - QBrush grey_brush(Qt::gray); - for (int col = 0; col < 6; ++col) - child_item->setForeground(col, grey_brush); + QBrush grey_brush(Qt::gray); + for (int col = 0; col < 6; ++col) + child_item->setForeground(col, grey_brush); - // Show which slave is linked - for (Element *linked : elmt->linkedElements()) { - if (elmt->groupIndexForElement(linked) == i) { - child_item->setToolTip(0, - tr("Lié à: %1").arg(linked->actualLabel())); - break; + // Show which slave is linked + for (Element *linked : elmt->linkedElements()) { + if (elmt->groupIndexForElement(linked) == i) { + child_item->setToolTip(0, + tr("Lié à: %1").arg(linked->actualLabel())); + break; + } } - } - child_item->setFlags(child_item->flags() & ~Qt::ItemIsSelectable); + child_item->setFlags(child_item->flags() & ~Qt::ItemIsSelectable); + } + } else { + all_children_hidden = false; } } + + // If checkbox is on and every child is linked (hidden), hide the master too + if (m_hide_linked_cb->isChecked() && all_children_hidden) { + parent_item->setHidden(true); + } } } @@ -369,3 +394,10 @@ void PlcLinkWidget::on_m_show_this_pb_clicked() m_element->diagram()->showMe(); m_element->setHighlighted(true); } + +void PlcLinkWidget::on_m_hide_linked_cb_toggled(bool checked) +{ + QSettings settings; + settings.setValue(QStringLiteral("plclinkwidget/hideLinked"), checked); + buildPlcTree(); +} diff --git a/sources/ui/plclinkwidget.h b/sources/ui/plclinkwidget.h index 62ca2243c..a3f74ed20 100644 --- a/sources/ui/plclinkwidget.h +++ b/sources/ui/plclinkwidget.h @@ -29,6 +29,7 @@ class QTreeWidget; class QLineEdit; class QPushButton; class QLabel; +class QCheckBox; class Element; /** @@ -64,11 +65,13 @@ class PlcLinkWidget : public AbstractElementPropertiesEditorWidget void on_m_tree_widget_customContextMenuRequested(const QPoint &pos); void on_m_unlink_pb_clicked(); void on_m_show_this_pb_clicked(); + void on_m_hide_linked_cb_toggled(bool checked); private: QLabel *m_label{nullptr}; QPushButton *m_unlink_pb{nullptr}; QPushButton *m_show_this_pb{nullptr}; + QCheckBox *m_hide_linked_cb{nullptr}; QLineEdit *m_search_field{nullptr}; QTreeWidget *m_tree_widget{nullptr}; QLabel *m_hidden_masters_label{nullptr}; From 3c1c29f6c30b312503279cf34a471d783c52b04f Mon Sep 17 00:00:00 2001 From: Kellermorph Date: Sun, 13 Sep 2026 21:32:34 +0200 Subject: [PATCH 02/18] connect searchfield --- sources/ui/plclinkwidget.cpp | 23 ++++++++++++++++++----- sources/ui/plclinkwidget.h | 2 ++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/sources/ui/plclinkwidget.cpp b/sources/ui/plclinkwidget.cpp index 4949be237..b0736f273 100644 --- a/sources/ui/plclinkwidget.cpp +++ b/sources/ui/plclinkwidget.cpp @@ -175,6 +175,7 @@ void PlcLinkWidget::buildPlcTree() { m_tree_widget->clear(); m_io_entry_hash.clear(); + m_linked_children.clear(); if (!m_element || !m_element->diagram() || !m_element->diagram()->project()) return; @@ -248,6 +249,7 @@ void PlcLinkWidget::buildPlcTree() // If this IO is already linked to a slave if (used_io_indices.contains(i)) { + m_linked_children.insert(child_item); if (m_hide_linked_cb->isChecked()) { child_item->setHidden(true); } else { @@ -288,6 +290,11 @@ void PlcLinkWidget::buildPlcTree() } } +bool PlcLinkWidget::isChildLinked(QTreeWidgetItem *child) const +{ + return m_linked_children.contains(child); +} + void PlcLinkWidget::hideButtons() { m_label->hide(); @@ -321,15 +328,21 @@ void PlcLinkWidget::on_m_search_field_textEdited(const QString &text) } } } - child->setHidden(!match); - if (match) any_child_visible = true; + bool hidden = m_hide_linked_cb->isChecked() && isChildLinked(child); + child->setHidden(!match || hidden); + if (match && !hidden) any_child_visible = true; } // Also check if parent label matches if (!text.isEmpty() && parent->text(0).contains(text, Qt::CaseInsensitive)) { - any_child_visible = true; - for (int j = 0; j < parent->childCount(); ++j) - parent->child(j)->setHidden(false); + for (int j = 0; j < parent->childCount(); ++j) { + QTreeWidgetItem *child = parent->child(j); + bool hidden = m_hide_linked_cb->isChecked() && isChildLinked(child); + if (!hidden) { + any_child_visible = true; + child->setHidden(false); + } + } } parent->setHidden(!any_child_visible); diff --git a/sources/ui/plclinkwidget.h b/sources/ui/plclinkwidget.h index a3f74ed20..37a4a9cb3 100644 --- a/sources/ui/plclinkwidget.h +++ b/sources/ui/plclinkwidget.h @@ -59,6 +59,7 @@ class PlcLinkWidget : public AbstractElementPropertiesEditorWidget void buildPlcTree(); void hideButtons(); void showButtons(); + bool isChildLinked(QTreeWidgetItem *child) const; private slots: void on_m_search_field_textEdited(const QString &text); @@ -82,6 +83,7 @@ class PlcLinkWidget : public AbstractElementPropertiesEditorWidget int ioIndex = -1; }; QHash m_io_entry_hash; + QSet m_linked_children; Element *m_element_to_link = nullptr; int m_pending_io_index = -1; From fcd2a4e0e033fb7cc6da696823b11e0af44e2f6d Mon Sep 17 00:00:00 2001 From: ispyisail Date: Mon, 14 Sep 2026 14:55:17 +1200 Subject: [PATCH 03/18] Fix bugtracker #248: opening a file while QET is running does nothing QET::splitWithSpaces() split on QRegularExpression("[^\\]?(?:\\\\)* "). That is not a valid pattern: "[^\\]" opens a character class whose "\\]" is an escaped bracket, so the class is never closed. QRegularExpression reported isValid() == false, QString::split() warned "invalid QRegularExpression object", and the function returned an EMPTY list for every input. It is the receiving half of the SingleApplication handshake: a secondary instance sends "launched-with-args: " + joinWithSpaces(args) (main.cpp) and the running instance parses it in QETApp::receiveMessage() before calling openFiles(). With the split always empty, the running instance received no arguments at all -- so opening a project while QET was already running silently did nothing. The bug is reported against filenames containing spaces, which is how it was noticed, but it is not limited to them: plain names failed identically. A corrected regex is not available. The separator is a space preceded by an even-length run of backslashes, and PCRE2 has no variable-length lookbehind, so the run cannot be expressed in a lookbehind and anything that matches it by consumption eats the character before the space -- which is what the "[^\\]?" was for. Scanning the string explicitly is correct and easier to read. tests/qttest/tst_qetstrings.cpp asserts the round trip splitWithSpaces(joinWithSpaces(x)) == x over plain names, embedded spaces, embedded backslashes, a trailing backslash and a mixture, plus the specific regression that a plain argument list does not come back empty. Verified the test fails without the fix: 9 of 11 cases fail on the old implementation and all 11 pass with it. Full suite 5/5, Qt 5.15.18. Co-Authored-By: Claude Opus 5 --- sources/qet.cpp | 38 ++++++++++++++++++++--- tests/qttest/CMakeLists.txt | 13 ++++++++ tests/qttest/tst_qetstrings.cpp | 54 +++++++++++++++++++++++++++++++++ 3 files changed, 100 insertions(+), 5 deletions(-) create mode 100644 tests/qttest/tst_qetstrings.cpp diff --git a/sources/qet.cpp b/sources/qet.cpp index a4c5dee52..84982c179 100644 --- a/sources/qet.cpp +++ b/sources/qet.cpp @@ -546,12 +546,40 @@ QString QET::joinWithSpaces(const QStringList &string_list) { QStringList QET::splitWithSpaces(const QString &string) { // les chaines sont separees par des espaces non echappes // = avec un nombre nul ou pair de backslashes devant - - QStringList escaped_strings = string.split(QRegularExpression("[^\\]?(?:\\\\)* "),Qt::SkipEmptyParts); - + // + // This was a QRegularExpression("[^\\]?(?:\\\\)* ") split, which never + // worked: "[^\]" opens a character class whose "\]" is an escaped + // bracket, so the class is never closed and the pattern is invalid. + // QRegularExpression::isValid() was false, QString::split() warned + // "invalid QRegularExpression object" and returned an EMPTY list for + // every input -- so a second instance's file arguments were always + // dropped (bugtracker #248), not just ones containing spaces. + // + // A correct pattern is not expressible here either: the separator is a + // space preceded by an even-length run of backslashes, and PCRE2 has no + // variable-length lookbehind. Scanning explicitly is both correct and + // easier to read than the alternatives. QStringList returned_list; - foreach(QString escaped_string, escaped_strings) { - returned_list << QET::unescapeSpaces(escaped_string); + QString current; + int backslashes = 0; + for (const QChar &c : string) { + if (c == QLatin1Char('\\')) { + ++backslashes; + current += c; + continue; + } + if (c == QLatin1Char(' ') && backslashes % 2 == 0) { + if (!current.isEmpty()) { + returned_list << QET::unescapeSpaces(current); + } + current.clear(); + } else { + current += c; + } + backslashes = 0; + } + if (!current.isEmpty()) { + returned_list << QET::unescapeSpaces(current); } return(returned_list); } diff --git a/tests/qttest/CMakeLists.txt b/tests/qttest/CMakeLists.txt index 4fec2b9a3..11bbde5c7 100644 --- a/tests/qttest/CMakeLists.txt +++ b/tests/qttest/CMakeLists.txt @@ -100,3 +100,16 @@ add_executable( add_test(NAME tst_smart_device COMMAND tst_smart_device) target_include_directories(tst_smart_device PRIVATE ${QET_DIR}/sources) target_link_libraries(tst_smart_device PRIVATE Qt::Test Qt::Sql) + +# qet.cpp carries the SingleApplication argument wire format +# (joinWithSpaces/splitWithSpaces); it pulls in qeticons and shortcutmanager, +# so those are compiled alongside rather than linking the whole application. +add_executable( + tst_qetstrings + tst_qetstrings.cpp + ${QET_DIR}/sources/qet.cpp + ${QET_DIR}/sources/qeticons.cpp + ${QET_DIR}/sources/shortcutmanager.cpp) +add_test(NAME tst_qetstrings COMMAND tst_qetstrings) +target_include_directories(tst_qetstrings PRIVATE ${QET_DIR}/sources) +target_link_libraries(tst_qetstrings PRIVATE Qt::Test Qt::Widgets Qt::Xml) diff --git a/tests/qttest/tst_qetstrings.cpp b/tests/qttest/tst_qetstrings.cpp new file mode 100644 index 000000000..f7e68011d --- /dev/null +++ b/tests/qttest/tst_qetstrings.cpp @@ -0,0 +1,54 @@ +#include + +#include "qet.h" + +/** + QET::joinWithSpaces() / QET::splitWithSpaces() are the wire format for the + SingleApplication message a secondary instance sends to the running one + (main.cpp: "launched-with-args: " + joinWithSpaces(...), received by + QETApp::receiveMessage()). If the round trip loses arguments, opening a + file while QET is already running silently does nothing. + + splitWithSpaces() used to split on QRegularExpression("[^\\]?(?:\\\\)* "), + which is not a valid pattern: "[^\\]" opens a character class whose "\\]" + is an escaped bracket, so the class never closes. QRegularExpression + reported isValid() == false and QString::split() returned an empty list for + every input -- bugtracker #248. +*/ +class tst_qetstrings : public QObject +{ + Q_OBJECT + +private slots: + void roundTrips_data() + { + QTest::addColumn("input"); + + QTest::newRow("single plain") << QStringList{"one.qet"}; + QTest::newRow("two plain") << QStringList{"one.qet", "two.qet"}; + QTest::newRow("space in name") << QStringList{"my file.qet"}; + QTest::newRow("space then plain") << QStringList{"my file.qet", "other.qet"}; + QTest::newRow("spaces in path") << QStringList{"/home/a b/c d.qet", "/tmp/x.qet"}; + QTest::newRow("backslash in name") << QStringList{"back\\slash.qet"}; + QTest::newRow("trailing backslash") << QStringList{"trailing\\"}; + QTest::newRow("mixed") << QStringList{"a b", "c\\d", "e"}; + } + + /// What the IPC actually needs: whatever went in comes back out. + void roundTrips() + { + QFETCH(QStringList, input); + QCOMPARE(QET::splitWithSpaces(QET::joinWithSpaces(input)), input); + } + + /// The specific regression: the old implementation returned an empty list + /// for every input, so this passed nothing on to openFiles(). + void splitIsNotEmptyForPlainArguments() + { + QVERIFY(!QET::splitWithSpaces(QStringLiteral("one.qet")).isEmpty()); + QCOMPARE(QET::splitWithSpaces(QStringLiteral("a.qet b.qet")).count(), 2); + } +}; + +QTEST_APPLESS_MAIN(tst_qetstrings) +#include "tst_qetstrings.moc" From 3665ec1bcd8bf13ff7add75dd1951fae3b0862b6 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Mon, 14 Sep 2026 15:15:39 +1200 Subject: [PATCH 04/18] Fix bugtracker #108: the junction dot vanishes on a wide conductor Conductor::paint() drew every junction as a fixed 3.0-unit ellipse, regardless of how wide the conductor carrying it is. The conductor width is user-settable from 0.4 to 20.0, so at anything above about 3.0 the dot is narrower than the line it sits on and disappears entirely -- exactly when a junction most needs to be legible. The dot now scales with m_properties.cond_size, floored at the historic 3.0 so nothing changes at or below the default width of 1.0. Only the wide conductors the report is about are affected. cond_size is used rather than the pen width because the pen is inflated by 4 while the mouse is over the conductor; the junction should not grow on hover. Measured with a temporary trace over examples/741.qet: at the default width the diameter stays 3.00, and with condsize="5" it becomes 15.00. Visually, a PNG export of that widened project shows two junctions that were invisible under the line rendering as clear dots. ctest 4/4, Qt 5.15.18. Co-Authored-By: Claude Opus 5 --- sources/qetgraphicsitem/conductor.cpp | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/sources/qetgraphicsitem/conductor.cpp b/sources/qetgraphicsitem/conductor.cpp index 5711b5d1f..9d37ac387 100644 --- a/sources/qetgraphicsitem/conductor.cpp +++ b/sources/qetgraphicsitem/conductor.cpp @@ -574,8 +574,23 @@ void Conductor::paint(QPainter *painter, const QStyleOptionGraphicsItem *options painter -> setPen(final_conductor_pen); painter -> setBrush(junction_brush); painter -> setRenderHint(QPainter::Antialiasing, true); + // The junction dot has to read as a dot on top of the conductor + // that carries it, so it scales with the conductor width instead + // of being a fixed 3.0 across: on a wide conductor a 3.0 dot is + // narrower than the line and simply disappears (bugtracker #108). + // + // Floored at the historic 3.0 so nothing changes for the default + // width of 1.0 or anything thinner -- only the wide conductors + // the report is about are affected. m_properties.cond_size is + // used rather than the pen, whose width is inflated by 4 while + // the mouse is over the conductor. + const qreal junction_diameter = qMax(3.0, 3.0 * m_properties.cond_size); + const qreal junction_radius = junction_diameter / 2.0; foreach(QPointF point, junctions_list) { - painter -> drawEllipse(QRectF(point.x() - 1.5, point.y() - 1.5, 3.0, 3.0)); + painter -> drawEllipse(QRectF(point.x() - junction_radius, + point.y() - junction_radius, + junction_diameter, + junction_diameter)); } } From ce890da342aaa17e2d0a5890abb610ad3bc769bc Mon Sep 17 00:00:00 2001 From: ispyisail Date: Mon, 14 Sep 2026 21:27:57 +1200 Subject: [PATCH 05/18] Open forwarded files outside the socket handler, not inside it QETApp::receiveMessage() called openFiles() directly. That slot runs inside SingleApplication's socket handling: SingleApplicationPrivate:: slotDataAvailable() emits receivedMessage synchronously from the readyRead lambda (singleapplication_p.cpp:452). openFiles() then loads a project -- seconds of work on a large one -- and openAndAddProject() puts up a modal BackupDialog whose exec() runs a nested event loop while the socket handler is still on the stack. During that nested loop the secondary instance exits, the connection closes and the QLocalSocket is deleted. When the dialog is dismissed and the stack unwinds, QMetaObject::activate() carries on emitting on the freed sender and the process dies. A zero-timer returns to the event loop first, so the socket stack is fully unwound before any project is opened. Found by scorpio810 while testing PR #861, with a backtrace showing no QET frame above the crash. His second suggestion, looking for a delete that should be deleteLater(), turned out to be already satisfied at singleapplication_p.cpp:331 -- which is why the deferred delete is not enough on its own once a nested loop is in play. Dismissing the dialog is the step that makes it fail: two earlier attempts to reproduce it left the dialog open, the stack never unwound, and nothing crashed. With the dialog dismissed it segfaults twice out of two; with this change it survives twice out of two, opens the project as before, and ctest stays green. Qt 6.10.2 on X11/xcb -- also checked under a headless Wayland compositor and under Qt 5.15.18, so it is neither Wayland-specific nor a Qt6 regression. The crash needs PR #861 to be reachable at all: without it splitWithSpaces() returns an empty list, no project opens, and nothing enters this path. Co-Authored-By: Claude Opus 5 --- sources/qetapp.cpp | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/sources/qetapp.cpp b/sources/qetapp.cpp index 3de743760..9afb2e300 100644 --- a/sources/qetapp.cpp +++ b/sources/qetapp.cpp @@ -1659,7 +1659,31 @@ void QETApp::receiveMessage(int instanceId, QByteArray message) { QString my_message(str.mid(20)); QStringList args_list = QET::splitWithSpaces(my_message); - openFiles(QETArguments(args_list)); + + // Deferred, not called directly. + // + // This slot runs inside SingleApplication's readyRead handling: + // SingleApplicationPrivate::slotDataAvailable() emits + // receivedMessage() synchronously from the socket's readyRead + // lambda. openFiles() then loads a project -- seconds of work on + // a large one -- and openAndAddProject() puts up a modal + // BackupDialog, whose exec() runs a nested event loop while the + // socket handler is still on the stack. + // + // During that nested loop the secondary instance exits, the + // connection closes and the QLocalSocket is deleted. When the + // dialog is dismissed and the stack unwinds, QMetaObject:: + // activate() continues emitting on the freed sender and the + // process dies. Reported with a backtrace on PR #861; + // reproduced on Qt 6.10.2 by dismissing the dialog, which is the + // step that makes it fail -- leaving it open never unwinds. + // + // A zero-timer returns to the event loop first, so the socket + // stack is fully unwound before any of this runs. + const QETArguments deferred_args{args_list}; + QTimer::singleShot(0, this, [this, deferred_args]() { + openFiles(deferred_args); + }); } } From 561b9c4eb113b83761f8779fe730ad7514083381 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Mon, 14 Sep 2026 22:14:36 +1200 Subject: [PATCH 06/18] Fix indentation of the deferred-open comment block The comment sat one tab deeper than the code around it. Flagged in review on PR #868. Whitespace only; no change to behaviour. Co-Authored-By: Claude Opus 5 --- sources/qetapp.cpp | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/sources/qetapp.cpp b/sources/qetapp.cpp index 9afb2e300..c02d99850 100644 --- a/sources/qetapp.cpp +++ b/sources/qetapp.cpp @@ -1660,26 +1660,26 @@ void QETApp::receiveMessage(int instanceId, QByteArray message) QString my_message(str.mid(20)); QStringList args_list = QET::splitWithSpaces(my_message); - // Deferred, not called directly. - // - // This slot runs inside SingleApplication's readyRead handling: - // SingleApplicationPrivate::slotDataAvailable() emits - // receivedMessage() synchronously from the socket's readyRead - // lambda. openFiles() then loads a project -- seconds of work on - // a large one -- and openAndAddProject() puts up a modal - // BackupDialog, whose exec() runs a nested event loop while the - // socket handler is still on the stack. - // - // During that nested loop the secondary instance exits, the - // connection closes and the QLocalSocket is deleted. When the - // dialog is dismissed and the stack unwinds, QMetaObject:: - // activate() continues emitting on the freed sender and the - // process dies. Reported with a backtrace on PR #861; - // reproduced on Qt 6.10.2 by dismissing the dialog, which is the - // step that makes it fail -- leaving it open never unwinds. - // - // A zero-timer returns to the event loop first, so the socket - // stack is fully unwound before any of this runs. + // Deferred, not called directly. + // + // This slot runs inside SingleApplication's readyRead handling: + // SingleApplicationPrivate::slotDataAvailable() emits + // receivedMessage() synchronously from the socket's readyRead + // lambda. openFiles() then loads a project -- seconds of work on + // a large one -- and openAndAddProject() puts up a modal + // BackupDialog, whose exec() runs a nested event loop while the + // socket handler is still on the stack. + // + // During that nested loop the secondary instance exits, the + // connection closes and the QLocalSocket is deleted. When the + // dialog is dismissed and the stack unwinds, QMetaObject:: + // activate() continues emitting on the freed sender and the + // process dies. Reported with a backtrace on PR #861; + // reproduced on Qt 6.10.2 by dismissing the dialog, which is the + // step that makes it fail -- leaving it open never unwinds. + // + // A zero-timer returns to the event loop first, so the socket + // stack is fully unwound before any of this runs. const QETArguments deferred_args{args_list}; QTimer::singleShot(0, this, [this, deferred_args]() { openFiles(deferred_args); From 428687ee4b4ddc02f93a40fe3da655f8c889ee1c Mon Sep 17 00:00:00 2001 From: Laurent Trinques Date: Mon, 14 Sep 2026 12:45:28 +0200 Subject: [PATCH 07/18] Update ca translations, thanks Antoni --- ELEMENTS.LICENSE | 30 +- lang/qet_ca.ts | 3170 +++++++++++++++++++++------------------------- 2 files changed, 1480 insertions(+), 1720 deletions(-) diff --git a/ELEMENTS.LICENSE b/ELEMENTS.LICENSE index 4ac88b654..ed97f9c28 100644 --- a/ELEMENTS.LICENSE +++ b/ELEMENTS.LICENSE @@ -1,3 +1,20 @@ +[ca] +La col·lecció d'elements que s'inclou amb QElectrotech es proporciona tal com és +i sense cap garantia que sigui adequada per al vostre ús o que funcioni correctament. +L'ús, la modificació i la integració d'aquests elements en esquemes elèctrics +estan permesos sense restriccions, independentment de la llicència final que regeixi +els esquemes. +No es permet utilitzar aquest programari ni cap fitxer associat +com a dades de mostra per crear models d'aprenentatge automàtic. + +Si redistribuïu la totalitat o una part de la col·lecció QElectroTech, +amb o sense modificacions, fora d'un esquema elèctric, heu de complir +les condicions de la llicència CC-BY: +Aquesta obra està subjecta a la llicència Reconeixement 3.0, +disponible en línia a http://creativecommons.org/licenses/by/3.0/ o bé +sol·licitant-la per correu a Creative Commons, 171 Second Street, Suite 300, San Francisco, +Califòrnia 94105, EUA. + [en] The elements collection provided along with QElectroTech is provided as is and without any warranty of fitness for your purpose or working. @@ -96,19 +113,6 @@ Para ver una copia de esta licencia, visite http://creativecommons.org/licenses/by/3.0/ o envie una carta a Creative Commons, 171 Second Street, Suite 300, San Francisco, California 94105, USA. -[ca] -La col·lecció de símbols QElectrotech és distribuïda tal qual i sense cap -garantia d'idoneïtat d'ús ni de funcionament. -Es permet incondicionalment, amb independència de la llicència final, emprar, -editar, i incloure aquests símbols en esquemes elèctrics. -Si vostè redistribueix una part de la col·lecció de QElectrotech o tota ella, -amb condicions o sense, separadament d'un esquema elèctric, haurà de respectar -les condicions de la llicència CC-BY: -Aquesta obra es troba sota una llicència Reconeixement 3.0 de Creative Commons. -Per veure una còpia d'aquesta llicència visiti -http://creativecommons.org/licenses/by/3.0/ o enviï una carta a Creative -Commons, 171 Second Street, Suite 300, San Francisco, California 94105, - [cs] Sbírka prvků poskytovaná společně s QElectroTechem je poskytována tak, jak je, bez záruky nebo vhodnosti pro váš účal nebo práci. diff --git a/lang/qet_ca.ts b/lang/qet_ca.ts index 1c6c5c4fa..36def8a57 100644 --- a/lang/qet_ca.ts +++ b/lang/qet_ca.ts @@ -46,7 +46,7 @@ Licenses - + Llicències @@ -224,7 +224,7 @@ Traduction en coréen - + Traducció al coreà @@ -507,45 +507,45 @@ Valeur - Valor + Valor Incrément - + Increment Suivant - Següent + Següent Réinitialiser à la valeur de départ - + Restablir al valor inicial Valeur actuelle du compteur. Saisir une nouvelle valeur et valider pour la modifier. - + Valor actual del comptador. Introduïu un valor nou i confirmeu-lo per canviar-lo. Incrément : valeur ajoutée au compteur à chaque nouvelle numérotation - + Increment: valor que s'afegeix al comptador amb cada nova operació de numeració Prochaine valeur qui sera appliquée avec cet incrément - + Següent valor a aplicar amb aquest increment @@ -575,7 +575,7 @@ Autonumérotation - Numeració automàtica + Numeració automàtica @@ -732,43 +732,46 @@ Nota: Aquestes opcions NO permeten ni bloquegen les numeracions automàtiques, n Formata com a llista de material - + nomenclature_ nomenclatura_ - + Enregister sous... Desa com a... - + Fichiers csv (*.csv) Fitxers csv (*.csv) - + Erreur Error - - Impossible d'enregistrer la nomenclature dans %1. -%2 - + + Impossible de remplacer le fichier! + + + No es pot substituir el fitxer! + + - + Position Posició - + Position du folio Posició del full - + Quantité numéro d'article Special field with name : designation quantity Quantitat de número d'article @@ -780,25 +783,25 @@ Nota: Aquestes opcions NO permeten ni bloquegen les numeracions automàtiques, n Créer une copie de sauvegarde ? window title - + Crea una còpia de seguretat? Souhaitez-vous créer une copie de sauvegarde ? dialog message - + Voleu crear una còpia de seguretat? Oui yes button - + Sí Non no button - + No @@ -849,7 +852,7 @@ Nota: Aquestes opcions NO permeten ni bloquegen les numeracions automàtiques, n Cliquez pour choisir une couleur - + Fes clic per triar un color @@ -874,16 +877,16 @@ Nota: Aquestes opcions NO permeten ni bloquegen les numeracions automàtiques, n Conductor - + Modifier les propriétés d'un conducteur undo caption - Modifica les propietats d'un conductor + Edita les propietats d'un conductor - + Modifier les propriétés de plusieurs conducteurs undo caption - Modifica les propietats de diversos conductors + Edita les propietats de diversos conductors @@ -902,13 +905,13 @@ Nota: Aquestes opcions NO permeten ni bloquegen les numeracions automàtiques, n Modifier les propriétés d'un conducteur undo caption - Modifica les propietats d'un conductor + Edita les propietats d'un conductor Modifier les propriétés de plusieurs conducteurs undo caption - Modifica les propietats de diversos conductors + Edita les propietats de diversos conductors @@ -1176,124 +1179,124 @@ Nota: Aquestes opcions NO permeten ni bloquegen les numeracions automàtiques, n Sélectionner un groupe de contacts - + Selecciona un grup de contactes Sélectionnez le groupe de contacts à assigner à cet élément esclave : - + Seleccioneu el grup de contactes que voleu assignar a aquest element esclau: # - + # Type - Tipus + Tipus Sous-type - + Subtipus Contacts - + Contactes Bornes - + Borns T%1 - + T%1 (déjà assigné) - + (ja assignat) (état ne correspond pas) - + (la condició no coincideix) (sous-type ne correspond pas) - + (el subtipus no coincideix) (nombre de contacts ne correspond pas) - + (el nombre de contactes no coincideix) OK - + D'acord Annuler - + Cancel·la Normalement ouvert - Normalment obert + Normalment obert Normalement fermé - Normalment tancat + Normalment tancat Inverseur - Inversor + Inversor Autre - + Altres Inconnu - + Desconegut Simple - Unifilar + Simple Puissance - Potència + Potència Temporisé travail - Temporitzador a la connexió + Temporitzador a la connexió Temporisé repos - Temporitzador a la desconnexió + Temporitzador a la desconnexió Temporisé travail & repos - Temporitzador a la connexió i a la desconnexió + Temporitzador a la connexió i a la desconnexió @@ -1309,17 +1312,17 @@ Nota: Aquestes opcions NO permeten ni bloquegen les numeracions automàtiques, n nom_de_la_propriete - + nom_de_la_propietat Lettres minuscules, chiffres, tiret et underscore uniquement - + Només lletres minúscules, números, guions i guions baixos Supprimer cette propriété - + Suprimeix aquesta propietat @@ -1327,33 +1330,33 @@ Nota: Aquestes opcions NO permeten ni bloquegen les numeracions automàtiques, n Enregistrer... - + Desa... Enregistrer le rapport de diagnostic - + Desa l'informe de diagnòstic Fichiers texte (*.txt);;Tous les fichiers (*) - + Fitxers de text (*.txt);;Tots els fitxers (*) Erreur - Error + Error Impossible d'écrire dans le fichier « %1 ». - + No es pot escriure al fitxer «%1». Diagram - + Modifier la profondeur Canvia la profunditat @@ -1423,7 +1426,7 @@ Nota: Aquestes opcions NO permeten ni bloquegen les numeracions automàtiques, n Clic : positionner à la taille d'origine. Cliquer-glisser : positionner et redimensionner. Clic droit : pivoter de 90°. Ctrl+molette : ajuster la taille. - + Clic: estableix la mida original. Clic i arrossega: posiciona i canvia la mida. Clic amb el botó dret: gira 90°. Ctrl + roda del ratolí: ajusta la mida. @@ -1431,7 +1434,7 @@ Nota: Aquestes opcions NO permeten ni bloquegen les numeracions automàtiques, n Clic: point anguleux. Cliquer-glisser: point courbe. Clic sur le premier point: fermer. Échap/Entrée: terminer. Clic droit: annuler le dernier point. - + Clic: punt de cantonada. Clic i arrossegar: punt de corba. Clic al primer punt: tancar. Esc/Intro: finalitzar. Clic amb el botó dret: desfer l'últim punt. @@ -1439,262 +1442,195 @@ Nota: Aquestes opcions NO permeten ni bloquegen les numeracions automàtiques, n Clic gauche : positionner le point de départ (Ctrl = position libre) - + Clic esquerre: col·loca el punt d'inici (Ctrl = col·locació lliure) Clic gauche : positionner le premier coin (Ctrl = point central, position libre) - + Clic esquerre: situar el primer vèrtex (Ctrl = punt central, posicionament lliure) Clic gauche : positionner le premier point (Ctrl = position libre) - + Clic esquerre: col·loca el primer punt (Ctrl = col·locació lliure) Clic gauche : positionner le point final (Ctrl = position libre) ; clic droit : annuler - + Clic esquerre: situar el punt final (Ctrl = posicionament lliure); clic dret: cancel·lar Clic gauche : positionner le coin opposé (Maj = carré, Ctrl = depuis le centre + position libre, Ctrl+Maj = carré centré) ; clic droit : annuler - + Clic esquerre: situa el vèrtex oposat (Maj = quadrat, Ctrl = des del centre + posicionament lliure, Ctrl+Maj = quadrat centrat); clic dret: cancel·la Clic gauche : positionner le coin opposé (Maj = cercle, Ctrl = depuis le centre + position libre, Ctrl+Maj = cercle centré) ; clic droit : annuler - + Clic esquerre: situa el vèrtex oposat (Maj = cercle, Ctrl = des del centre + posicionament lliure, Ctrl+Maj = cercle centrat); clic dret: cancel·la Clic gauche : point suivant ; double-clic ou Entrée : terminer ; clic droit : annuler le dernier point - + Clic esquerre: punt següent; doble clic o Retorn: finalitzar; clic dret: cancel·lar l'últim punt DiagramImageItem - + redimensionner - + canvia la mida - + pivoter/incliner - + gira/inclinació - + Cliquer : mode %1 - + Clic: mode %1 - + Glisser un coin/bord : redimensionner (Ctrl = depuis le centre, Maj = conserver les proportions) - + Arrossega una cantonada o una vora: canvia la mida (Ctrl = des del centre, Maj = manté les proporcions) - + Glisser un coin : pivoter (Maj = par pas de 15°) ; glisser un bord : incliner (Maj = par pas de 15°) ; point rouge : déplacer le centre de rotation - + Arrossega una cantonada: gira (Maj = increments de 15°); arrossega una aresta: inclina (Maj = increments de 15°); punt vermell: mou el centre de rotació - + -- %1 : mode %2 - + -- %1 : mode %2 - + Cliquer - + Fer clic - + Glisser : redimensionner (Maj = conserver les proportions, Ctrl = depuis le centre) - + Arrossega: canvia la mida (Maj = manté la proporció, Ctrl = des del centre) - + Glisser : pivoter (Maj = par pas de 15°) - + Arrossega: gira (Maj = increments de 15°) - + Glisser : incliner (Maj = par pas de 15°) - + Arrossega: inclina (Maj = increments de 15°) - + Glisser : déplacer le centre de rotation - + Arrossega: mou el centre de rotació - - Redimensionner une image - - - - - Faire pivoter une image - - - - - Incliner une image - - - - + Déplacer le centre de rotation d'une image - + Mou el centre de rotació d'una imatge - + Modifier une image - + Edita una imatge - + Restaurer les proportions d'une image - + Restaura les proporcions d'una imatge - - - Enregistrer l'image sous... - - - - - - Enregistrer l'image d'origine sous... - - - - - Image PNG (*.png) - - - - - Image JPEG (*.jpg *.jpeg) - - - - - Image BMP (*.bmp) - - - - - Image SVG (*.svg) - - - - - Tous les fichiers (*) - - - - - Transparence non conservée - - - - - Ce format ne prend pas en charge la transparence : l'image sera enregistrée telle qu'elle était avant l'application de la couleur transparente. Continuer ? - - - - - Échec de l'enregistrement - - - - - Impossible d'enregistrer l'image à cet emplacement. - - - - + une image una imatge - + Remplacer l'image... - + Substitueix la imatge... - + Couleur transparente... - + Color transparent... - + Rogner... - + Retalla... - + Miroir horizontal - + Mirall horitzontal - + Miroir vertical - + Mirall vertical - + Restaurer les proportions - + Restaura les proporcions - + + Propriétés... + Propietats... + + + Selectionner une image... - Selecciona una imatge ... + Selecciona una imatge ... - + Image Files (*.png *.jpg *.jpeg *.bmp *.svg) - + Fitxers d'imatge (*.png *.jpg *.jpeg *.bmp *.svg) - + Erreur - Error + Error - + Impossible de charger l'image. - No es pot carregar la imatge. + No es pot carregar la imatge. - + Remplacer une image - + Substitueix una imatge - + Miroir horizontal d'une image - + Mirall horitzontal d'una imatge - + Miroir vertical d'une image - + Mirall veritcal d'una imatge - + Définir une couleur transparente - + Estableix un color transparent - + Rogner une image - + Retalla una imatge @@ -1717,7 +1653,8 @@ Nota: Aquestes opcions NO permeten ni bloquegen les numeracions automàtiques, n <Shift> to move - + +<Majús> per moure's @@ -1744,7 +1681,7 @@ Nota: Aquestes opcions NO permeten ni bloquegen les numeracions automàtiques, n Créer un template context menu action - + Crea una plantilla @@ -1765,23 +1702,24 @@ Nota: Aquestes opcions NO permeten ni bloquegen les numeracions automàtiques, n Modèle enregistré - + Plantilla desada Le modèle a été enregistré avec succès sous : %1 - + La plantilla s'ha desat correctament com a: +%1 Erreur - Error + Error Le fichier n'a pas pu être écrit. - + No s'ha pogut escriure el fitxer. @@ -1792,7 +1730,7 @@ Nota: Aquestes opcions NO permeten ni bloquegen les numeracions automàtiques, n Modifier les propriétés d'un conducteur undo caption - Modifica les propietats d'un conductor + Edita les propietats d'un conductor @@ -1816,12 +1754,12 @@ Nota: Aquestes opcions NO permeten ni bloquegen les numeracions automàtiques, n DynamicElementTextItem - + Texte Text - + Éditer un texte d'élément Edita un text d'element @@ -2150,7 +2088,7 @@ Nota: Aquestes opcions NO permeten ni bloquegen les numeracions automàtiques, n Tourner autour de son propre centre - + Gira al voltant del propi centre @@ -2188,66 +2126,66 @@ Nota: Aquestes opcions NO permeten ni bloquegen les numeracions automàtiques, n Font - - + + Déplacer un champ texte Mou un camp de text - + Pivoter un champ texte Gira un camp de text - + Modifier le texte d'un champ texte - Canvia el text d'un camp de text + Edita el text d'un camp de text - - + + Modifier la police d'un champ texte Canvia la font d'un camp de text - + Modifier le cadre d'un champ texte Canvia el marc d'un camp de text - + Modifier la largeur d'un texte Canvia l'amplada d'un text - + Modifier l'information d'un texte - Canvia la informació d'un text + Edita la informació d'un text - + Modifier la source de texte, d'un texte Canvia la font del text d'un text - + Modifier l'alignement d'un champ texte Canvia l'alineació d'un camp de text - + Modifier la couleur d'un champ texte Canvia el color d'un camp de text - + Modifier la conservation de l'angle Canvia la conservació de l'angle - + Modifier le point de rotation d'un champ texte - + Canvia el punt de rotació d'un camp de text @@ -2288,41 +2226,41 @@ Nota: Aquestes opcions NO permeten ni bloquegen les numeracions automàtiques, n File not found: %1 - + Fitxer no trobat: %1 Cannot read %1 - + No es pot llegir %1 This .edz is a zip-format package, which is not yet supported (only 7-Zip .edz files can be imported). - + Aquest .edz és un paquet en format zip que encara no és compatible (només es poden importar fitxers .edz de 7-Zip). Not a valid .edz package (unrecognised archive format). - + No és un paquet .edz vàlid (format d'arxiu no reconegut). Could not create a temporary directory: %1 - + No s'ha pogut crear un directori temporal: %1 No *.part.xml found inside the .edz - + No s'ha trobat cap *.part.xml dins del .edz Element - + Numéroter automatiquement un élément undo caption - + Numera automàticament un element @@ -2374,13 +2312,13 @@ Nota: Aquestes opcions NO permeten ni bloquegen les numeracions automàtiques, n Enregistrer un template dialog title - + Desa una plantilla Choisissez l'emplacement dans lequel vous souhaitez enregistrer votre template. dialog content - + Tria la ubicació on vols desar la plantilla. @@ -2400,19 +2338,19 @@ Nota: Aquestes opcions NO permeten ni bloquegen les numeracions automàtiques, n Nom du nouveau template - + Nom de la nova plantilla Écraser le template ? message box title - + Voleu sobreescriure la plantilla? Le template existe déjà. Voulez-vous l'écraser ? message box content - + La plantilla ja existeix. Vols sobreescriure-la? @@ -2430,14 +2368,15 @@ Nota: Aquestes opcions NO permeten ni bloquegen les numeracions automàtiques, n Nom de fichier de l'élément placeholder: the element's file name, not its display name - + Nom del fitxer de l'element Nom de fichier de l'élément : chiffres, minuscules, « - », « _ » et « . » uniquement. Le nom affiché de l'élément se modifie séparément dans les propriétés de l'élément. tooltip for the element file-name field - + Nom de fitxer de l'element: només dígits, lletres minúscules, "-", "_" i "." +El nom de visualització de l'element es modifica per separat a les propietats de l'element. @@ -2499,7 +2438,7 @@ Le nom affiché de l'élément se modifie séparément dans les propriété Exclure de la numérotation auto - + Exclou de la numeració automàtica @@ -2509,17 +2448,17 @@ Le nom affiché de l'élément se modifie séparément dans les propriété Ajouter une propriété personnalisée - + Afegeix una propietat personalitzada Séparation de potentiel - + Separació de potencial Exclure de la nomenclature - + Excloure de la nomenclatura @@ -2531,10 +2470,10 @@ Le nom affiché de l'élément se modifie séparément dans les propriété - - - - + + + + Type Tipus @@ -2545,405 +2484,383 @@ Le nom affiché de l'élément se modifie séparément dans les propriété - - - margin: 5px; font-weight: bold; - - - - - Exclure de la nomenclature - - - - Élément esclave Element esclau - + Nombre de contact représenté Nombre de contactes representats - + Type de contact Tipus del contacte - + État du contact Estat del contacte - + Élément maître Element mestre - + Type concret Tipus concret - + Définir le nombre maximal d'esclaves - + Estableix el nombre màxim d'esclaus - + Définir les éléments esclave - + Defineix els elements esclaus + + + + Contact + Contacte + + + + Nb. contacts + Nombre de contactes - Contact - - - - - Nb. contacts - - - - Nb. bornes - + Nombre de borns - + Élément bornier Element del born - - - + + + Fonction Funció - - Verrouiller la numérotation automatique - - - - - Séparation de potentiel - - - - + Informations Dades - + Nom Nom - + Valeurs Valors - - - + + + Simple Unifilar - + Maître Mestre - + Esclave Esclau - + Renvoi de folio suivant Referència del full següent - + Renvoi de folio précédent Referència del full anterior - + Bornier Bloc de Borns - + Vignette Miniatura - + Définition de conducteur - + Definició de conductor - - + + Normalement ouvert Normalment obert - - + + Normalement fermé Normalment tancat - - + + Inverseur Inversor - + Other Altres - + Esclave PLC - + PLC esclau - - + + Puissance Potència - - + + Temporisé travail Temporitzador a la connexió - - + + Temporisé repos Temporitzador a la desconnexió - - + + Temporisé travail & repos Temporitzador a la connexió i a la desconnexió - + Bobine Bobina - + Organe de protection Unitat de protecció - + Commutateur / bouton Commutador / botó - + Module PLC - + Mòdul PLC - - + + Générique Genèric - + Fusible Fusible - + Séctionnable Seleccionable - + Diode Díode - + Phase Fase - + Neutre Neutre - - - - - + + + + + T%1 - + T%1 - + Autre - + Altres - + Configuration PLC - + Configuració del PLC - + + - + + - + - - + - - - + + Adresse - + Adreça - - + + Commentaire - Comentari + Comentari - + Réf. croisée - + Referència creuada - - + + Nb. - + Nombre. - + T1 - + T1 - + Police des en-têtes - + Tipus de lletra de la capçalera - + Configurer la police des en-têtes de colonnes - + Configura la font de la capçalera de la columna - + Police du texte - + Tipus de lletra - + Configurer la police du texte dans les cellules - + Configura el tipus de lletra del text a les cel·les - + Afficher les en-têtes sur la feuille - + Mostra les capçaleres al full - + Afficher ou masquer les en-têtes de colonnes du tableau PLC sur la feuille - + Mostra o amaga les capçaleres de columna de la taula PLC al full - + Saut %1 après: - + Omet %1 després de: + + + + Aucun + Cap + + + + H. ligne: + Línia H: + + + + + mm + mm + + + + Réf. + Ref. + + + + Nom personnalisé de la colonne (vide = par défaut) + Nom de columna personalitzat (buit = per defecte) - Aucun - Cap - - - - H. ligne: - - - - - - mm - - - - - Réf. - - - - - Nom personnalisé de la colonne (vide = par défaut) - + Visible + Visible - Visible - - - - Coller depuis le presse-papiers - + Enganxa des del porta-retalls - - + + Police des en-têtes: %1 %2pt - + Tipus de lletra de la capçalera: %1 %2 pt - - + + Police du texte: %1 %2pt - + Tipus de lletra: %1 %2 pt - + Police des en-têtes de colonnes - + Tipus de lletra de la capçalera de la columna - + Police du texte des cellules - + Tipus de lletra del text de la cel·la - + Terre Terra @@ -2951,122 +2868,106 @@ Le nom affiché de l'élément se modifie séparément dans les propriété ElementPropertiesWidget - + Général General - + Élement Element - + Nom : %1 Nom : %1 - + Folio : %1 Full : %1 - + Type : %1 Tipus: %1 - + Sous-type : %1 Subtipus: %1 - + Position : %1 Posició : %1 - + Rotation : %1° Rotació : %1° - + Dimensions : %1*%2 Mida: %1*%2 - + Bornes : %1 Borns : %1 - - Nombre maximum de contacts esclaves définis : non défini - - - - - + Nombre maximum de contacts esclaves définis : %1 - + Nombre màxim de contactes esclaus definits: %1 + - + Nombre de contacts esclaves utilisés : %1 - + Nombre de contactes esclaus utilitzats: %1 + - - Contacts : NO : %1/%2, NC : %3/%4, inverseurs : %5/%6, autres : %7/%8 - - - - - - Contacts : NO : %1, NC : %2, inverseurs : %3, autres : %4 - - - - - + Emplacement : %1 Ubicació: %1 - + Verrouiller la position - Bloqueja la posició + Bloqueja la posició - + Retrouver dans le panel Cerca al panel - + Éditer l'élément Modifica l'element @@ -3191,7 +3092,7 @@ Le nom affiché de l'élément se modifie séparément dans les propriété Automates (MAE/SPS) - + PLC (MAE/SPS) @@ -3247,24 +3148,24 @@ Le nom affiché de l'élément se modifie séparément dans les propriété Moure una primitiva - + Éditer les informations sur l'auteur window title - Canvia les dades de l'autor + Edita les dades de l'autor - + Vous pouvez utiliser ce champ libre pour mentionner les auteurs de l'élément, sa licence, ou tout autre renseignement que vous jugerez utile. Pots utilitzar aquest camp lliure per esmentar els autors de l'element, la seva llicència, o qualsevol altra informació que consideris útil. - + Éditer les noms window title Modifica els noms - + Vous pouvez spécifier le nom de l'élément dans plusieurs langues. Pots especificar el nom de l'element en diferents llengües. @@ -3417,7 +3318,7 @@ Le nom affiché de l'élément se modifie séparément dans les propriété Importer une pièce EPLAN (.edz)… - + Importa un component d'EPLAN (.edz)... @@ -3489,7 +3390,7 @@ S'eliminaran tots els elements i carpetes contingudes en aquesta carpeta. Avertissement — Importation d'un fichier EPLAN (.edz) - + Avís — Importació d'un fitxer EPLAN (.edz) @@ -3505,38 +3406,50 @@ En important ce fichier, vous confirmez que : • vous connaissez son origine et êtes autorisé à l'utiliser dans ce contexte, au regard des conditions applicables à cette source ; • cette importation est effectuée à vos propres risques et responsabilité ; • ni QElectroTech, ni ses mainteneurs, ni ses contributeurs ne peuvent être tenus responsables d'une utilisation non conforme de ces données. - + El format .edz pot provenir de dues fonts diferents: + +• L'EPLAN Data Portal (dataportal.eplan.com), subjecte a les condicions d'ús de l'entorn EPLAN Cloud; +• El lloc web d'un fabricant (o distribuïdor) de components que ofereix els seus fitxers .edz directament, subjecte als seus propis termes i condicions. + +QElectroTech no pot determinar automàticament l'origen del fitxer que esteu important ni les condicions que s'hi apliquen. + +En importar aquest fitxer, confirmeu que: + +• en coneixeu l'origen i esteu autoritzats a utilitzar-lo en aquest context, d'acord amb els termes aplicables a aquesta font; +• aquesta importació es realitza sota el vostre propi risc i responsabilitat; +• ni QElectroTech, ni els seus responsables de manteniment, ni els seus col·laboradors poden ser considerats responsables de cap ús d'aquestes dades que no en respecti la normativa. J'ai lu et j'accepte ces conditions. - + He llegit i accepto aquestes condicions. Importer - + Importar Importer une pièce EPLAN - + Importa un component EPLAN Pièces EPLAN (*.edz) - + Components EPLAN (*.edz) Import EPLAN - + Importació d'EPLAN Impossible d'importer cette pièce : %1 - + No s'ha pogut importar aquesta component: +%1 @@ -3572,17 +3485,17 @@ En important ce fichier, vous confirmez que : Rechercher... - + Cerca... Collections - Col·leccions + Col·leccions Modèles - + Models @@ -3694,17 +3607,17 @@ En important ce fichier, vous confirmez que : Insérer un folio au-dessus - + Insereix un full a sobre Insérer un folio en dessous - + Insereix una pàgina a sota Copier et coller - + Copia i enganxa @@ -3756,7 +3669,7 @@ En important ce fichier, vous confirmez que : Panneau des éléments - + Panell Elements @@ -3870,61 +3783,49 @@ En important ce fichier, vous confirmez que : Dimensions - + Noms des fichiers cibles message box title Noms dels fitxers de destinació - + Vous devez entrer un nom de fichier non vide et unique pour chaque folio à exporter. message box content Has d'introduir un nom de fitxer únic i no buit per a cada full per exportar. - + Dossier non spécifié message box title No has indicat una carpeta - + Vous devez spécifier le chemin du dossier dans lequel seront enregistrés les fichiers images. message box content Has d'indicar el camí de la carpeta on es desaran les imatges. - - Images non incluses dans l'export DXF - message box title - - - - - Le format DXF utilisé ici (AC1006) ne permet pas d'inclure d'image. Les images seront représentées uniquement par un rectangle de contour (position, taille, rotation et inclinaison conservées), sans le contenu de l'image. - message box content - - - - + Impossible d'écrire dans ce fichier message box title No es pot escriure en aquest fitxer - + Il semblerait que vous n'ayez pas les permissions nécessaires pour écrire dans le fichier %1. message box content No sembla disposar dels permisos necessaris per escriure al fitxer %1. - + Aperçu Vista prèvia - - + + px px @@ -4016,7 +3917,7 @@ En important ce fichier, vous confirmez que : Dessiner les noms des bornes - + Dibuixa els noms dels borns @@ -4259,7 +4160,7 @@ Les entrades de text i nombres també Modifier des propriétés de borniers - Modifica les propietats del bloc de borns + Edita les propietats del bloc de borns @@ -4367,12 +4268,12 @@ Les entrades de text i nombres també Afficher la grille par défaut (appliqué au prochain lancement) - + Mostra la quadrícula per defecte (s'aplicarà en la propera execució) Afficher les guides par défaut (appliqué au prochain lancement) - + Mostra les guies per defecte (s'aplicaran en la propera execució) @@ -4437,7 +4338,7 @@ Les entrades de text i nombres també Répertoire des Macros utilisateur - + Directori de macros d'usuari @@ -4568,29 +4469,29 @@ Podeu especificar el valor per defecte d'aquest camp per als elements que c Affichage Grille - + Vista de quadrícula max: - + max: Taille des points de la grille de Diagram-Editor : 1 - 5 - + Mida del punt de la quadrícula de l'editor de diagrames: 1 - 5 min: - + min: Taille des points de la grille de l'éditeur d'éléments : 1 - 5 - + Mida del punt de la quadrícula de l'editor d'elements: 1 - 5 @@ -4716,7 +4617,7 @@ Podeu especificar el valor per defecte d'aquest camp per als elements que c Coréen - + Coreà @@ -4786,7 +4687,7 @@ Podeu especificar el valor per defecte d'aquest camp per als elements que c Suédois - + Suec @@ -4816,7 +4717,7 @@ Podeu especificar el valor per defecte d'aquest camp per als elements que c Chemin des macros utilisateur - + Camí de la macro d'usuari @@ -4826,7 +4727,7 @@ Podeu especificar el valor per defecte d'aquest camp per als elements que c Fonctionnalité expérimental - + Funcionalitat experimental @@ -4836,7 +4737,12 @@ Toutes valeurs autre que ‘Pas d’arrondi’ peut causer des erreurs de rendu 1 - la valeur sélectionnée 2 - du dpi de l'écran 3 - Modifier le projet sur un autre ordinateur et/ou écran n'ayant pas les mêmes paramètres des points 1 et 2. - + AVÍS: +Qualsevol configuració que no sigui «Sense arrodoniment» pot provocar errors en el renderitzat del projecte en funció de: + +1 - La configuració seleccionada +2 - Els DPI de la pantalla +3 - L'edició del projecte en un altre ordinador i/o pantalla que no tingui la mateixa configuració per als punts 1 i 2. @@ -5083,38 +4989,38 @@ Falten %1 línies per mostrar tota la informació Orientation - + Orientació Position - Posició + Posició Couleur - Color + Color Ajouter - Afegeix + Afegeix Supprimer - Suprimeix + Suprimeix Horizontal - Horitzontal + Horitzontal Vertical - Vertical + Vertical @@ -5122,22 +5028,22 @@ Falten %1 línies per mostrar tota la informació Rogner l'image - + Retalla la imatge Faites glisser les poignées, ou l'intérieur du cadre, pour ajuster la zone à conserver. - + Arrossegueu els tiradors, o l'interior del marc, per ajustar l'àrea que voleu conservar. Réinitialiser - + Restablir Revenir à l'image complète, sans rognage - + Torna a la imatge completa, sense retallar-la @@ -5150,51 +5056,51 @@ Falten %1 línies per mostrar tota la informació Largeur - Amplada + Amplada % - + % Verrouillé : modifier la largeur ou la hauteur ajuste l'autre pour conserver les proportions. Cliquer pour déverrouiller. - + Bloquejat: canviar l'amplada o l'alçada ajusta l'altra dimensió per mantenir la relació d'aspecte. Feu clic per desbloquejar. Hauteur - Alçada + Alçada Restaurer les proportions - + Restaurar les proporcions Angle - + Angle ° - + ° Inclinaison X - + Inclinació en l'eix X Inclinaison Y - + Inclinació en l'eix Y @@ -5204,71 +5110,71 @@ Falten %1 línies per mostrar tota la informació Modifier la largeur d'une image - + Canvia l'amplada d'una imatge Modifier la hauteur d'une image - + Canvia l'alçada d'una imatge Modifier l'angle d'une image - + Canvia l'angle d'una imatge Modifier l'inclinaison d'une image - + Canvia la inclinació d'una imatge Déverrouillé : largeur et hauteur peuvent être modifiées indépendamment. Cliquer pour verrouiller. - + Desbloquejat: l'amplada i l'alçada es poden canviar de manera independent. Feu clic per bloquejar-ho. ImageTransparentColorDialog - + Couleur transparente - + Color transparent - + Image source - + Imatge d'origen - + Aperçu - + Vista prèvia - + rgb(%1, %2, %3) - + rgb(%1, %2, %3) - + Tolérance pour cette couleur - + Tolerància per a aquest color - + Retirer cette couleur - + Elimina aquest color - + Cliquez sur l'image pour ajouter une couleur. Ajustez la tolérance de chaque couleur avec son curseur, ou cliquez sur × pour la retirer. - + Feu clic a la imatge per afegir un color. Ajusteu la tolerància de cada color amb el seu lliscador o feu clic a × per eliminar-lo. - + Cliquez sur l'image pour choisir une couleur - + Fes clic a la imatge per triar un color @@ -5404,7 +5310,7 @@ Si us plau, feu servir l'editor avançat per a això. Modifier un champ texte - Canvia un camp de text + Edita un camp de text @@ -5499,12 +5405,12 @@ Si us plau, feu servir l'editor avançat per a això. Atteindre un élément window title - + Accedir a un element Nom, label ou information de l'élément… - + Nom, etiqueta o informació de l'element... @@ -5612,7 +5518,7 @@ Si us plau, feu servir l'editor avançat per a això. Remarque : les éléments maîtres ayant atteint leur nombre maximal d'esclaves sont masqués. - + Nota: Els elements mestres que han assolit el nombre màxim d'elements esclaus estan ocults. @@ -5625,119 +5531,119 @@ Si us plau, feu servir l'editor avançat per a això. Veure l'element enllaçat - + Lier l'élément Enllaça l'element - + Montrer l'élément Mostra l'element - + Montrer l'élément esclave Mostra l'element esclau - + Enregistrer la disposition Desa el disseny - + Report de folio Informe del full - + Référence croisée (esclave) Referència creuada (esclau) + - Label Etiqueta + - Commentaire Comentari - - + + Label de folio Etiqueta del full + - - - + + Position Posició + - - - + + Titre de folio Títol del full - - + + N° de folio Núm. de full - - + + N° de fil N° de fil - - + + Fonction Funció - - + + Tension / Protocole Tensió / Protocol - - + + Couleur du conducteur Color del conductor - - + + Section du conducteur Secció del conductor - + (déjà utilisé) - + (ja utilitzat) - + Sélectionner un IO PLC - + Seleccioneu una E/S de PLC - + IO disponible: - + E/S disponible: @@ -5854,51 +5760,49 @@ Si us plau, feu servir l'editor avançat per a això. Desa el disseny - + Nombre maximal d'esclaves atteint. - + S'ha assolit el nombre màxim d'esclaus. - - La limite fixée pour cet élément maître est atteinte (Limite: %1). - -Voulez-vous tout de même lier ce contact esclave ? - + + Cet élément maître ne peut plus accepter aucun nouveau contact esclave, la limite fixée a été atteinte (Limite: %1). + Aquest element mestre ja no pot acceptar més contactes esclaus; s'ha assolit el límit establert (Límit: %1). - + Type - Tipus + Tipus - + Adresse - + Adreça - + Fonction - Funció + Funció - + Commentaire - Comentari + Comentari - + Réf. croisée - + - + Bornes - + Borns - + Coller depuis le presse-papiers - + Enganxa des del porta-retalls @@ -6026,7 +5930,7 @@ Voulez-vous tout de même lier ce contact esclave ? Guides - + Guies @@ -6143,27 +6047,27 @@ Voulez-vous tout de même lier ce contact esclave ? Mettre à 0 pour un chiffre qui n'avance que par le report d'un chiffre cyclique suivant (ex: le "0" de "0.7") - + Establiu-ho a 0 per a un dígit que només avança mitjançant el transport del dígit cíclic següent (p. ex., el «0» a «0,7») Valeur à laquelle ce chiffre revient à 0 en incrémentant le chiffre précédent (0 = pas de cycle) - + Valor al qual aquest dígit es reinicia a 0 mentre s'incrementa el dígit anterior (0 = sense reinici cíclic) mod. - + mod. Format d'affichage : une suite de zéros donne le nombre minimum de chiffres (00 = 07, 000 = 007). Vide = largeur naturelle du type. - + Format de visualització: una seqüència de zeros especifica el nombre mínim de dígits (00 = 07, 000 = 007). Buit = amplada natural del tipus. 0 - + 0 @@ -6199,7 +6103,7 @@ Voulez-vous tout de même lier ce contact esclave ? Cyclique (modulo) - + Cíclic (mòdul) @@ -6208,7 +6112,7 @@ Voulez-vous tout de même lier ce contact esclave ? Alphabétique - + Alfabètic @@ -6371,7 +6275,7 @@ Voulez-vous tout de même lier ce contact esclave ? Déplacer le label d'une borne - + Mou una etiqueta de born @@ -6384,7 +6288,7 @@ Voulez-vous tout de même lier ce contact esclave ? Modifier un champ texte - Modifica un camp de text + Edita un camp de text @@ -6392,45 +6296,45 @@ Voulez-vous tout de même lier ce contact esclave ? Sélectionner une page - + Selecciona una pàgina Ce document PDF contient %n page(s) - - - + + Aquest document PDF conté %n pàgina + Aquest document PDF conté %n pàgines Page à importer : - + Pàgina a importar: Résolution : - + Resolució : 150 DPI (écran) - + 150 ppp (pantalla) 300 DPI (impression) - + 300 ppp (impressió) 600 DPI (haute qualité) - + 600 ppp (alta qualitat) Aperçu : - + Vista prèvia : @@ -6438,72 +6342,72 @@ Voulez-vous tout de même lier ce contact esclave ? Cet élément est déjà lié - Aquest element ja està enllaçat + Aquest element ja està enllaçat Délier - Desvincular + Desvincular Voir cet élément - Veure aquest element + Veure aquest element Recherche - Recerca + Recerca Label - Etiqueta + Etiqueta Type - Tipus + Tipus Adresse - + Adreça Fonction - Funció + Funció Commentaire - Comentari + Comentari Bornes - + Borns Remarque : les éléments maîtres ayant atteint leur nombre maximal d'esclaves sont masqués. - + Nota: Els elements mestres que han assolit el nombre màxim d'elements esclaus estan ocults. Automate (PLC) - + Autòmat programable (PLC) Lié à: %1 - + Vinculat a: %1 Connecter - + Connecta @@ -6661,7 +6565,7 @@ Secció del conductor: %1 Modifier les propriétés de plusieurs conducteurs undo caption - Modifica les propietats de diversos conductors + Edita les propietats de diversos conductors @@ -6687,56 +6591,56 @@ Les variables següents són incompatibles: ProjectAutoNumConfigPage - + Numérotation auto Numeració automàtica - + Management Administració - + Conducteurs Conductors - + Eléments Elements - + Folios Fulls - + Numérotation auto des folios Numeració automàtica de fulls - - - - - - - + + + + + + + Sans nom Sense nom - - - - - - - - - + + + + + + + + + Nom de la nouvelle numérotation Nom de la nova numeració @@ -6744,12 +6648,12 @@ Les variables següents són incompatibles: ProjectDBModel - + Position Posició - + Position du folio Posició de full @@ -6802,26 +6706,26 @@ Les variables següents són incompatibles: Temps passé sur ce projet : label when configuring - + Temps dedicat a aquest projecte: Suivre le temps passé sur ce projet (uniquement enregistré localement dans ce fichier) checkbox label - + Registra el temps dedicat a aquest projecte (emmagatzemat només localment en aquest fitxer) Réinitialiser button label - + Restablir %1 h %2 min hours and minutes of time spent on a project - + %1 h %2 min @@ -6889,12 +6793,12 @@ Les variables següents són incompatibles: Dessiner les noms des bornes - + Dibuixa els noms dels borns Inscrire les informations des composants - + Introduïu la informació del component @@ -7069,7 +6973,7 @@ Vol desar els canvis? Êtes-vous sûr de vouloir supprimer ce folio du projet ? message box content - + Esteu segur que voleu eliminar aquest foli del projecte? @@ -7122,12 +7026,12 @@ Vol desar els canvis? go one page left - + Ves una pàgina a l'esquerra go one page right - + Ves una pàgina a la dreta @@ -7188,220 +7092,221 @@ Vol desar els canvis? Carregant... Obrint els fitxers - + LTR Translate this string to RTL if you are translating to a Right-to-Left language, else translate to LTR LTR - + Cartouches QET title of the title block templates collection provided by QElectroTech Caixetins QET - + Cartouches company title of the company's title block templates collection Caixetins d'empresa - + Cartouches utilisateur title of the user's title block templates collection Caixetins personals - + Q Single-letter example text - translate length, not meaning Q - + QET Small example text - translate length, not meaning QET - + Schema Normal example text - translate length, not meaning Esquema - + Electrique Normal example text - translate length, not meaning Elèctric - + QElectroTech Long example text - translate length, not meaning QElectroTech - + Configurer QElectroTech window title Configura QElectroTech - + Chargement... splash screen caption Carregant... - + Chargement... icône du systray splash screen caption Carregant... icona de la safata del sistema - + QElectroTech systray menu title QElectroTech - + &Quitter &Sortir - + &Masquer &Amagar - + &Restaurer &Restaurar - + &Masquer tous les éditeurs de schéma Amaga tots els editors d'&esquemes - + &Restaurer tous les éditeurs de schéma Restaura tots els editors d'&esquemes - + &Masquer tous les éditeurs d'élément &Amaga tots els editors d'elements - + &Restaurer tous les éditeurs d'élément &Restaura tots els editors d'elements - + &Masquer tous les éditeurs de cartouche systray submenu entry Amaga tots els editors de &caixetins - + &Restaurer tous les éditeurs de cartouche systray submenu entry Restaura tots els editors de &caixetins - + &Nouvel éditeur de schéma &Nou editor d'esquemes - + &Nouvel éditeur d'élément &Nou editor d'elements - + Ferme l'application QElectroTech Tanca el programa QElectroTech - + Réduire QElectroTech dans le systray Minimitza QEletectroTech a la safata del sistema - + Restaurer QElectroTech Restaura QElectroTech - + QElectroTech systray icon tooltip QElectroTech - + Éditeurs de schémas Editors d'esquemes - + Éditeurs d'élément Editors d'elements - + Éditeurs de cartouche systray menu entry Editors de caixetins - + <b>Le fichier de restauration suivant a été trouvé,<br>Voulez-vous l'ouvrir ?</b><br> <b>S'ha trobat el següent fitxer de restauració.<br>Voleu obrir-lo?</b><br> - + <b>Les fichiers de restauration suivant on été trouvé,<br>Voulez-vous les ouvrir ?</b><br> <b>S'han trobat els fitxers de restauració següents.<br>Voleu obrir-los?</b><br> - + Fichier de restauration Fitxer de recuperació - + Rapport de plantage - + Informe d'error - + QElectroTech ne s'est pas fermé correctement lors de sa dernière exécution. Voici les derniers messages enregistrés avant l'arrêt -- vous pouvez les enregistrer pour les joindre à un rapport de bug. - + QElectroTech no es va tancar correctament durant la seva última execució. +Aquí teniu els darrers missatges enregistrats abans de l'apagada; podeu desar-los per adjuntar-los a un informe d'errors. - + Rapport de diagnostic - + Informe de diagnòstic - + Ceci contient les derniers messages de journalisation de cette session. Vérifiez le contenu avant de le joindre à un rapport de bug public. - + Això conté els missatges de registre més recents d'aquesta sessió. Reviseu-ne el contingut abans d'adjuntar-lo a un informe d'error públic. - + Usage : Ús : - + [options] [fichier]... @@ -7410,7 +7315,7 @@ Voici les derniers messages enregistrés avant l'arrêt -- vous pouvez les - + QElectroTech, une application de réalisation de schémas électriques. Options disponibles : @@ -7427,35 +7332,35 @@ Opcions disponibles: - + --common-elements-dir=DIR Definir le dossier de la collection d'elements --common-elements-dir=DIR Definir la carpeta de la col·lecció d'elements - + --common-tbt-dir=DIR Definir le dossier de la collection de modeles de cartouches --common-tbt-dir=DIR Definir la carpeta de la col·lecció de caixetins - + --config-dir=DIR Definir le dossier de configuration --config-dir=DIR Definir la carpeta de configuració - + --data-dir=DIR Definir le dossier de data --data-dir=DIR Defineix la carpeta de dades - + --lang-dir=DIR Definir le dossier contenant les fichiers de langue --lang-dir=DIR Definir la carpeta amb els fitxers de llengua @@ -7465,1044 +7370,1040 @@ Opcions disponibles: QETDiagramEditor - - - + + + QElectroTech window title QElectroTech - + QElectroTech status bar message QElectroTech - + Annulations dock title Desfés - + Aucune modification Cap canvi - + Cliquez sur une action pour revenir en arrière dans l'édition de votre schéma Status tip Trieu una acció per retrocedir en l'edició de l'esquema - + &Nouveau &Nou - + &Ouvrir &Obre - + &Fermer &Tanca - + &Enregistrer &Desa - + Enregistrer sous Desa com a - + E&xporter E&xporta - + Ce panneau liste les différentes actions effectuées sur le folio courant. Cliquer sur une action permet de revenir à l'état du schéma juste après son application. "What's this" tip Aquest panell mostra les diferents accions realitzades en el full actual. Faci clic en una acció per tornar a l'estat del full just després de l'acció. - - - - - + + + + + + - - - - - + + + + + + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + Éditeur de schémas - + Editor d'esquemes - + Imprimer Imprimeix - + Exporter en pdf Exporta com a pdf - + Exporte un ou plusieurs folios du projet courant status bar tip Exporta un o més fulls del projecte actual - + &Quitter &Surt - + Annuler Desfés - + Refaire Refés - + Co&uper Re&talla - + Cop&ier Cop&ia - + C&oller En&ganxa - + Coupure automatique de conducteur(s) Tool tip of auto break conductor - + Desconnexió automàtica del/s conductor/s - + Couper automatiquement les conducteurs existants lors du placement d'un élément Status tip of auto break conductor - + Tallar automàticament els conductors existents en col·locar un element - + Couleur de fond blanc/gris Tool tip of white/grey background button Color de fons blanc / gris - + Affiche la couleur de fond du folio en blanc ou en gris Status tip of white/grey background button Mostra el color de fons del full en blanc o gris - + Afficher la grille Mostra la quadrícula - + Affiche ou masque la grille des folios Mostra o oculta la quadrícula - + Afficher les guides - + Mostra les guies - + Affiche ou masque les guides - + Mostra o amaga les guies - + Propriétés du folio Propietats del full - + Ajouter un folio Afegeix un full - - - + + + Supprimer le folio Suprimeix un full - + Exporter au format CSV Exporta en format CSV - + Ajouter une nomenclature Afegeix una nomenclatura - + Gestionnaire de borniers (DEV) Gestor de blocs de borns (DEV) - + Lancer le plugin de création de borniers Inicieu el connector de creació de blocs de borns - + Exporter la liste des noms de conducteurs Exporta la llista de noms de conductors - + Exporter le plan de câblage - + Exporta l'esquema de cablejat - - Liste de câblage (base de données) - - - - + Numérotation automatique des bornes - + Numeració automàtica de borns - + Exporter la base de donnée interne du projet Exporta la base de dades interna del projecte - + Crée un nouveau projet status bar tip Crea un nou projecte - + Ouvre un projet existant status bar tip Obre un projecte existent - + Ferme le projet courant status bar tip Tanca el projecte actual - + Enregistre le projet courant et tous ses folios status bar tip Desa el projecte actual i tots els seus fulls - + Enregistre le projet courant avec un autre nom de fichier status bar tip Desa el projecte actual amb un altre nom de fitxer - + Ajouter une ligne Add row Afegeix una línia - + Enlever une ligne Remove row Suprimeix una línia - + Ajoute une colonne au folio status bar tip Afegeix una columna al full - + Enlève une colonne au folio status bar tip Suprimeix una columna al full - + Agrandit le folio en hauteur status bar tip Augmenta l'altura del full - + Rétrécit le folio en hauteur status bar tip Disminueix l'altura del full - + Pivoter le groupe - + Gira el grup - + Grouper les textes sélectionnés Agrupa els textos seleccionats - + Enlève les éléments sélectionnés du folio status bar tip Suprimeix els elements seleccionats del full - + Pivote la sélection comme un groupe autour de son centre, au lieu de chaque élément sur place status bar tip - + Gira la selecció com a grup al voltant del seu centre, en lloc de girar cada element al seu lloc - + Tout sélectionner Selecciona-ho tot - + Désélectionner tout Desselecciona-ho tot - + Inverser la sélection Inverteix la selecció - + Ajouter un PDF - + Afegeix un PDF - + Ajouter une courbe - + Afegeix una corba - + Ajouter un plan de bornes Afegeix un pla de borns - + Ajoute un champ de texte sur le folio actuel Afegeix un camp de text al full actual - + Ajoute une image sur le folio actuel Afegeix una imatge al full actual - + Ajoute une page PDF sur le folio actuel - + Afegeix una pàgina PDF al foli actual - + Ajoute une ligne sur le folio actuel Afegeix una fila al full actual - + Ajoute un rectangle sur le folio actuel Afegeix un rectangle al full actual - + Ajoute une ellipse sur le folio actuel Afegeix una el·lipse al full actual - + Ajoute une polyligne sur le folio actuel Afegeix un polígon al full actual - + Ajoute une courbe de Bézier sur le folio actuel - + Afegeix una corba de Bézier al foli actual - + Ajoute un plan de bornier sur le folio actuel Afegeix un pla de borns al full actual - + Chercher/remplacer Cerca i reemplaça - + Atteindre un élément - + Accedir a un element - + Recherche et sélectionne rapidement un élément du folio status bar tip - + Cerca i selecciona ràpidament un element del foli - + Profondeur toolbar title Profunditat - + Supprimer Suprimeix - + Pivoter Gira - + Orienter les textes Orienta els textos - + Retrouver dans le panel Cerca al panell - + Réinitialiser les conducteurs Reinicia els conductors - + Ajouter un champ de texte Afegeix un camp de text - + Ajouter une colonne Afegeix una columna - + Ajouter une image Afegeix una imatge - + Enlever une colonne Suprimeix una columna - + Propriétés du projet Propietats del projecte - + Nettoyer le projet Neteja el projecte - + Ajouter un sommaire Afegeix un resum - + Zoom avant Apropa - + Zoom arrière Allunya - + Zoom sur le contenu Amplia el contingut - + Zoom adapté Zoom adaptat - + Pas de zoom Sense zoom - + en utilisant des onglets En pestanyes - + en utilisant des fenêtres En finestres - + Mode Selection Mode Selecció - + Mode Visualisation Mode Visualització - + &Mosaïque &Mosaic - + &Cascade &Cascada - + Projet suivant Projecte següent - + Projet précédent Projecte anterior - + Sélectionne tous les éléments du folio status bar tip Selecciona tots els elements del full - + Désélectionne tous les éléments du folio status bar tip Deselecciona tots els elements del full - + Agrandit le folio status bar tip Augmenta la mida del full - + Rétrécit le folio status bar tip Disminueix la mida del full - + Adapte le zoom de façon à afficher tout le contenu du folio indépendamment du cadre Ajusta el zoom per mostrar tot el contingut del full independentment del marc - + Adapte le zoom exactement sur le cadre du folio status bar tip Ajusta el zoom exactament al marc del full - + Ajouter un rectangle Afegeix un rectangle - + Ajouter une ellipse Afegeix una el·lipse - + Ajouter une polyligne Afegeix un polígon - + Retrouve l'élément sélectionné dans le panel status bar tip Troba l'element seleccionat en el panell - + Exporte le folio courant dans un autre format status bar tip Exporta el full actual a un altre format - + Imprime un ou plusieurs folios du projet courant status bar tip Imprimeix un o més fulls del projecte actual - + Place les éléments du presse-papier sur le folio status bar tip Enganxa els elements del porta-retalls al full - + Édite les propriétés du folio (dimensions, informations du cartouche, propriétés des conducteurs...) status bar tip Edita les propietats del full (dimensions, informació del caixetí, propietats dels conductors, etc.) - + Permet de visualiser le folio sans pouvoir le modifier status bar tip Permet veure el full sense poder modificar-lo - - + + Projet %1 enregistré dans le repertoire: %2. Projecte %1 desat al directori: %2. - + Projets QElectroTech (*.qet);;Fichiers XML (*.xml);;Tous les fichiers (*) Projectes QElectroTech (* .qet);; Arxius XML (* .xml);; Tots els fitxers (*) - - + + Impossible d'ouvrir le fichier message box title Impossible obrir el fitxer - + Il semblerait que le fichier %1 que vous essayez d'ouvrir n'existe pas ou plus. Sembla que el fitxer %1 que estàs intentant obrir no existeix. - + Ouverture du projet en lecture seule message box title Obertura del projecte en mode de només lectura - + %n description(s) de police écrite(s) dans un format étranger ou corrompu ont été restaurée(s). Elles seront réécrites dans un format stable au prochain enregistrement du projet. message box content - - - + + S'ha restaurat %n descripció de tipus de lletra que estava en un format estranger o corrupte. Es reescriurà en un format estable la propera vegada que es desi el projecte. + S'han restaurat %n descripcions de tipus de lletra que estaven en un format estranger o corrupte. Es reescriuran en un format estable la propera vegada que es desi el projecte. - + %n description(s) de police n'ont pas pu être lue(s) ; la police par défaut sera utilisée pour ces textes. message box content - - - + + No s'ha pogut llegir %n descripció de tipus de lletra; s'utilitzarà el tipus de lletra per defecte per a aquests textos. + No s'han pogut llegir %n descripcions de tipus de lletra; s'utilitzarà el tipus de lletra per defecte per a aquests textos. - + Polices du projet message box title - + Tipografies del projecte - + Une erreur est survenue lors de l'ouverture du fichier %1. message box content S'ha produït un error en obrir el fitxer %1. - + Active le projet « %1 » Activa el projecte « %1 » - + Êtes-vous sûr de vouloir supprimer ce folio ? - + Esteu segur que voleu eliminar aquest foli? - + Supprimer les folios - + Elimina els números de pàgina - + Êtes-vous sûr de vouloir supprimer les %1 folios sélectionnés ? - + Esteu segur que voleu suprimir els %1 folis seleccionats? - + Supprimer %1 folios - + Suprimeix %1 fulls - - - - - - - + + + + + + + Déplacer les folios - + Mou els fulls - + Erreur message box title Error - + Ferme l'application QElectroTech status bar tip Tanca el programa QElectroTech - + Annule l'action précédente status bar tip Desfer l'acció anterior - + Restaure l'action annulée status bar tip Restaura l'acció desfeta - + Transfère les éléments sélectionnés dans le presse-papier status bar tip Transfereix els elements seleccionats al porta-retalls - + Copie les éléments sélectionnés dans le presse-papier status bar tip Copia els elements seleccionats al porta-retalls - + Désélectionne les éléments sélectionnés et sélectionne les éléments non sélectionnés status bar tip Desselecciona els elements seleccionats i selecciona els elements no seleccionats - + Recalcule les chemins des conducteurs sans tenir compte des modifications status bar tip Recalcula les rutes dels conductors sense tenir en compte els canvis - + Pivote les éléments et textes sélectionnés status bar tip Gira els elements i textos seleccionats - + Éditer l'item sélectionné Edita l'element seleccionat - + Pivote les textes sélectionnés à un angle précis status bar tip Gira els textos seleccionats a un angle específic - + Création automatique de conducteur(s) Tool tip of auto conductor Creació automàtica de conductors - + Projets dock title Projectes - + Collections Col·leccions - + Utiliser la création automatique de conducteur(s) quand cela est possible Status tip of auto conductor Utilitza la creació automàtica de conductor (s) sempre que sigui possible - + Restaure le zoom par défaut status bar tip Restaura el nivell de zoom per defecte - + Ajouter une ligne Draw line Afegeix una línia - + Présente les différents projets ouverts dans des sous-fenêtres status bar tip Mostra els projectes oberts en subfinestres - + Présente les différents projets ouverts des onglets status bar tip Mostra els projectes oberts en pestanyes - + Permet de sélectionner les éléments status bar tip Permet seleccionar els elements - + Dispose les fenêtres en mosaïque status bar tip Disposa les finestres en mosaic - + Dispose les fenêtres en cascade status bar tip Disposa les finestres en cascada - + Active le projet suivant status bar tip Activa el projecte següent - + Active le projet précédent status bar tip Activa el projecte anterior - + &Fichier &Fitxer - + &Édition &Edita - + &Projet &Projecte - + Afficha&ge &Visualitza - + Fe&nêtres Fi&nestres - + &Récemment ouverts &Obre'n un de recent - + Affiche ou non la barre d'outils principale Mostra o amaga la barra d'eines principal - + Affiche ou non la barre d'outils Affichage Mostra o amaga la barra d'eines de visualització - + Affiche ou non la barre d'outils Schéma Mostra o amaga la barra d'eines d'esquema - + Affiche ou non le panel d'appareils Mostra o amaga el panell d'elements - + Affiche ou non la liste des modifications Mostra o amaga la llista de canvis - + Afficher les projets Mostra els projectes - + Outils Eines - + Affichage Visualitza - + Schéma Esquema - + Ajouter Afegeix - + Ouvrir un fichier Obre un fitxer - + Il semblerait que le fichier que vous essayez d'ouvrir ne soit pas accessible en lecture. Il est donc impossible de l'ouvrir. Veuillez vérifier les permissions du fichier. Sembla que el fitxer que intentes obrir no és llegible. Per tant, no es pot obrir. Si us plau, comprova els permisos del fitxer. - + Il semblerait que le projet que vous essayez d'ouvrir ne soit pas accessible en écriture. Il sera donc ouvert en lecture seule. Sembla que el projecte que intentes obrir no és accessible en mode d'escriptura. Per tant serà obert en mode de només lectura. - - + + Échec de l'ouverture du projet message box title Impossible obrir el projecte - + Il semblerait que le fichier %1 ne soit pas un fichier projet QElectroTech. Il ne peut donc être ouvert. message box content Sembla que el fitxer %1 no és un fitxer de projecte QElectroTech. Per tant no es pot obrir. - + Suppression de borne impossible - + No es pot suprimir el born - + La suppression ne peut être effectué car la selection possède une ou plusieurs bornes ponté et/ou appartenant à une borne à niveau multiple. Déponter et/ou supprimer les niveaux des bornes concerné afin de pouvoir les supprimer - + No es pot dur a terme l'eliminació perquè la selecció inclou un o més borns que estan connectats amb ponts i/o pertanyen a un born multinivell. +Retireu els ponts i/o elimineu els nivells dels borns afectats per tal de poder-los eliminar - + Groupe Grup - + Éditer l'élement edit element Edita l'element - + Éditer le champ de texte edit text field Edita el camp de text - + Éditer l'image edit image Edita la imatge - + Éditer le conducteur edit conductor Edita el conductor - + Éditer l'objet sélectionné edit selected item Edita l'objecte seleccionat @@ -8598,7 +8499,7 @@ Déponter et/ou supprimer les niveaux des bornes concerné afin de pouvoir les s Exporter en SVG - + Exporta com a SVG @@ -8703,7 +8604,7 @@ Déponter et/ou supprimer les niveaux des bornes concerné afin de pouvoir les s Fine-Rotation - + Rotació fina @@ -8814,12 +8715,12 @@ Déponter et/ou supprimer les niveaux des bornes concerné afin de pouvoir les s Nombre de bornes incorrect - + Nombre incorrecte de borns <br><b>Erreur</b> :<br>Les définitions de conducteur ne peuvent posséder qu'une seule borne.<br><b>Solution</b> :<br>Vérifier que l'élément ne possède qu'une seule borne - + <br><b>Error</b>:<br>Les definicions de conductor només poden tenir un born.<br><b>Solució</b>:<br>Verifiqueu que l'element només tingui un born @@ -8865,7 +8766,7 @@ Déponter et/ou supprimer les niveaux des bornes concerné afin de pouvoir les s Éditeur d'élément - + Editor d'elements @@ -8888,51 +8789,51 @@ Déponter et/ou supprimer les niveaux des bornes concerné afin de pouvoir les s Afegeix text d'elements editables als esquemes - + X: %1 Y: %2 - + X: %1 Y: %2 - + Exporter en SVG dialog title - + Exporta com a SVG - + Image SVG (*.svg) filetypes allowed when exporting an element to SVG - + Imatge SVG (*.svg) - + Échec de l'export - + L'exportació ha fallat - + Impossible d'écrire dans le fichier « %1 ». - + No es pot escriure al fitxer «%1». - + Avertissement Avís - + L'import d'un dxf volumineux peut prendre du temps veuillez patienter durant l'import... Importar un fitxer DXF gran pot trigar una estona. Si us plau, tingueu paciència mentre es completa el procés d'importació... - + Importer un élément à redimensionner Importa un element per canviar-ne la mida - + Éléments QElectroTech (*.elmt) Elements QElectroTech (*.elmt) @@ -8991,12 +8892,12 @@ Si us plau, tingueu paciència mentre es completa el procés d'importació. [només lectura] - + Aucune modification Cap canvi - + Éditeur d'éléments status bar message Editor d'elements @@ -9076,21 +8977,21 @@ Si us plau, tingueu paciència mentre es completa el procés d'importació. - + Impossible d'ouvrir le fichier %1. message box content No es pot obrir el fitxer %1. - + Ce fichier n'est pas un document XML valide message box content El fitxer no conté un document XML vàlid - + Erreur toolbar title Error @@ -9107,7 +9008,7 @@ Si us plau, tingueu paciència mentre es completa el procés d'importació. Vous n'avez pas les privilèges nécessaires pour modifier cet élement. Il sera donc ouvert en lecture seule. message box content - No disposa dels privilegis necessaris per modificar aquest element. Per tant, serà obert en mode de només lectura. + No disposa dels privilegis necessaris per editar aquest element. Per tant, serà obert en mode de només lectura. @@ -9140,41 +9041,41 @@ Si us plau, tingueu paciència mentre es completa el procés d'importació. Sembla que el fitxer %1 que estàs intentant obrir no existeix. - + Recharger l'élément dialog title Recarrega l'element - + Vous avez efffectué des modifications sur cet élément. Si vous le rechargez, ces modifications seront perdues. Voulez-vous vraiment recharger l'élément ? dialog content - l'element ha estat modificat. Si fos recarregat es perdrien els canvis. Segur que vol recarregar l'element? + L'element ha estat modificat. Si fos recarregat es perdrien els canvis. Segur que vol recarregar l'element? - - - + + + Echec de l'enregistrement Ha fallat el desament - - - + + + L'enregistrement à échoué, les conditions requises ne sont pas valides Ha fallat el desament, Les condicions requerides no són vàlides - + Enregistrer sous dialog title Desa com a - + Éléments QElectroTech (*.elmt) filetypes allowed when saving an element file Elements QElectroTech (*.elmt) @@ -9206,22 +9107,22 @@ Les condicions requerides no són vàlides - - + + Élément inexistant. message box title No existeix l'element. - + L'élément n'existe pas. message box content L'element no existeix. - + Le chemin virtuel choisi ne correspond pas à un élément. message box content El camí virtual escollit no correspon a cap element. @@ -9277,7 +9178,7 @@ Les condicions requerides no són vàlides Général - General + General @@ -9317,13 +9218,13 @@ Les condicions requerides no són vàlides Enregistrer un rapport de diagnostic... - + Desa un informe de diagnòstic... Génère un rapport avec les derniers messages de journalisation, pour l'inclure dans un rapport de bug status bar tip - + Genera un informe que conté els missatges de registre més recents, per incloure'l en un informe d'error @@ -9369,62 +9270,65 @@ Les condicions requerides no són vàlides QETProject - + Projet « %1 : %2» displayed title for a ProjectView - %1 is the project title, -%2 is the project path Projecte « %1 : %2» - + Projet %1 displayed title for a title-less project - %1 is the file name Projecte %1 - + Projet sans titre displayed title for a project-less, file-less project Projecte sense títol - + %1 [lecture seule] displayed title for a read-only project - %1 is a displayable title %1 [només lectura] - + %1 [modifié] displayed title for a modified project - %1 is a displayable title %1 [modificat] - + Système de contacts modifié - + Sistema de contactes modificat - + Le nouvel élément définit des groupes de contacts esclaves. Les éléments esclaves existants ne seront pas automatiquement assignés. Vous devrez relier manuellement les esclaves et assigner les groupes de contacts. Voulez-vous continuer ? - + El nou element defineix grups de contacte d'esclaus. +Els elements esclaus existents no s'assignaran automàticament. Haureu d'enllaçar manualment els esclaus i assignar-hi els grups de contacte. + +Voleu continuar? - + Une erreur s'est produite durant l'intégration du modèle. error message S'ha produït un error mentre s'integrava el model. - + Avertissement message box title Avís - + Ce document semble avoir été enregistré avec une version %1 qui est ultérieure à votre version ! Vous utilisez actuellement QElectroTech en version %2 @@ -9433,7 +9337,7 @@ Vous utilisez actuellement QElectroTech en version %2 Actualment esteu utilitzant la versió %2 de QElectroTech - + . Il est alors possible que l'ouverture de tout ou partie de ce document échoue. Que désirez vous faire ? @@ -9442,32 +9346,32 @@ Aleshores és possible que l'obertura de tot o part d'aquest document Què voleu fer? - + Avertissement message box title Avís - + Le projet que vous tentez d'ouvrir est partiellement compatible avec votre version %1 de QElectroTech. El projecte que esteu intentant obrir és parcialment compatible amb la vostra versió %1 de QElectroTech. - + Afin de le rendre totalement compatible veuillez ouvrir ce même projet avec la version 0.8, ou 0.80 de QElectroTech et sauvegarder le projet et l'ouvrir à nouveau avec cette version. Que désirez vous faire ? Perquè sigui totalment compatible, obriu aquest mateix projecte amb la versió 0.8 o 0.80 de QElectroTech, deseu el projecte i torneu-lo a obrir amb aquesta versió. Què voleu fer? - + <p align="center"><b>Ouverture du projet en cours...</b><br/>Création des folios</p> <p align="center"><b>Obrint el projecte actual...</b><br/>Creant els folis</p> - + <p align="center"><b>Ouverture du projet en cours...</b><br/>Mise en place des références croisées</p> <p align="center"><b>Obrint el projecte actual...</b><br/>Configurant referències creuades</p> @@ -9657,7 +9561,7 @@ Què voleu fer? Éditeur de cartouche - + Editor de caixetins @@ -9748,13 +9652,13 @@ Què voleu fer? Erreur message box title - Error + Error Impossible d'enregistrer le modèle « %1 ». message box content - %1 is a title block template name - + No s'ha pogut desar la plantilla «%1». @@ -9784,7 +9688,7 @@ Què voleu fer? Le nom « %1 » n'est pas valide : il ne doit pas contenir les caractères suivants : \ / : * ? " < > | message box content - %1 is the rejected template name - + El nom «%1» no és vàlid: no ha de contenir els caràcters següents: \ / : * ? " < > | @@ -9818,8 +9722,8 @@ Què voleu fer? QObject - - + + Avertissement : l'élément a été enregistré avec une version ultérieure de QElectroTech. Avís: l'element ha estat desat amb una versió posterior del QElectroTech. @@ -9867,13 +9771,13 @@ Què voleu fer? modifier le texte undo caption - modifica el text + edita el text modifier un conducteur undo caption - modifica un conductor + edita un conductor @@ -9885,13 +9789,13 @@ Què voleu fer? modifier le cartouche undo caption - modifica el caixetí + edita el caixetí modifier les dimensions du folio undo caption - modifica la mida del full + canvia la mida del full @@ -9900,85 +9804,85 @@ Què voleu fer? elimina - + couper des parties undo caption talla les parts - + déplacement undo caption moure - + modification noms undo caption - modifica noms + edita noms - + amener au premier plan undo caption porta al davant - + rapprocher undo caption apropa - + éloigner undo caption allunya - + envoyer au fond undo caption envia al fons - + modification informations complementaires undo caption - modifica informació complementària + edita informació complementària - + redimensionnement %1 undo caption -- %1 is the resized primitive type name canvi de mida %1 - + redimensionnement de %1 primitives undo caption -- %1 always > 1 Canvia la mida de les primitives %1 - + Modifier les propriétées de l'élément Edita les propietats de l'element - - + + Pivoter la selection undo caption Gira la selecció - + Miroir de sélection undo caption Mirall de selecció - + Retourner la sélection undo caption Torna la selecció @@ -10026,32 +9930,32 @@ Què voleu fer? text - + Folio sans titre Full sense títol - + schema esquema - + Conserver les proportions Manté les proporcions - + Réinitialiser les dimensions Torna a mides anteriors - + Aperçu Vista - + Exporter vers le presse-papier Exporta al portapapers @@ -10082,23 +9986,17 @@ Què voleu fer? %n camps de text - - - une image - part of a sentence listing the content of a diagram - una imatge - - - %n images + + %n image(s) part of a sentence listing the content of a diagram - - - + + %n imatge + %n imatges - + %n forme(s) part of a sentence listing the content of a diagram @@ -10107,7 +10005,7 @@ Què voleu fer? - + %n texte(s) d'élément part of a sentence listing the content of a diagram @@ -10116,7 +10014,7 @@ Què voleu fer? - + %n tableau(s) part of a sentence listing the content of diagram @@ -10125,7 +10023,7 @@ Què voleu fer? - + %n plan de bornes part of a sentence listing the content of a diagram @@ -10134,79 +10032,79 @@ Què voleu fer? - + Le fichier texte contenant la licence GNU/GPL est introuvable - bon bah de toute façon, vous la connaissez par coeur non ? No es troba el fitxer de text que conté la llicència GNU/GPL. Total... la coneixeu de memòria, oi? - + Le fichier texte contenant la licence GNU/GPL existe mais n'a pas pu être ouvert - bon bah de toute façon, vous la connaissez par coeur non ? El fitxer de text que conté la llicència GNU/GPL ha estat trobat però no ha estat possible obrir-lo. Total... la coneixeu de memòria, oi? - - - + + + Impossible d'ouvrir le fichier %1 en écriture, erreur %2 rencontrée. error message when attempting to write an XML file Impossible obrir el fitxer %1 en mode d'escriptura, error %2. - + Une erreur est survenue lors de l'écriture du fichier %1, erreur %2 rencontrée. error message when attempting to write an XML file S'ha produït un error en escriure el fitxer %1 i s'ha trobat l'error %2. - + Amener au premier plan Porta al primer pla - + Rapprocher Apropa - + Éloigner Allunya - + Envoyer au fond Envia al fons - + Ramène la ou les sélections au premier plan Torna a posar les seleccions al primer pla - + Rapproche la ou les sélections Apropa les seleccions - + Éloigne la ou les sélections Elimina les seleccions - + Envoie en arrière plan la ou les sélections Envia les seleccions al fons - - - - + + + + Profondeur - Profunditat + Profunditat - + Borne tooltip Born @@ -10281,7 +10179,7 @@ Què voleu fer? modification des informations complémentaires undo caption - modificació d'informacions complementàries + edita informacions complementàries @@ -10356,17 +10254,17 @@ Què voleu fer? ajustament horitzontal - + Auteur Autor - + Titre Títol - + Date Data @@ -10439,19 +10337,19 @@ Què voleu fer? - + Texte Text Modifier les informations de l'élément : %1 - Modifica les propietats de l'element: %1 + Edita les propietats de l'element: %1 Modifier les informations de plusieurs éléments - Modifica les propietats de diversos elements + Edita les propietats de diversos elements @@ -10463,20 +10361,20 @@ Què voleu fer? Modifier les propriétés d'un conducteur undo caption - Modifica les propietats d'un conductor + Edita les propietats d'un conductor Modifier les propriétés de plusieurs conducteurs undo caption - Modifica les propietats de diversos conductors + Edita les propietats de diversos conductors Numéroter automatiquement un conducteur undo caption - + Numerar automàticament un conductor @@ -10546,7 +10444,7 @@ Què voleu fer? Makros - + Macros @@ -10556,7 +10454,7 @@ Què voleu fer? Le fichier « %1 » est absent ou illisible : le nom traduit de ce dossier n'a pas pu être lu, son nom de dossier est affiché à la place. - + El fitxer «%1» no es troba o és il·legible: no s'ha pogut llegir el nom traduït d'aquesta carpeta, de manera que se'n mostra el nom de la carpeta. @@ -10599,457 +10497,431 @@ Què voleu fer? Orienta els textos seleccionats - + Fichier Fitxer - + Installation (=) Instal·lació (=) - - + + Localisation (+) Localització (+) - + Indice de révision Índex de revisió - + Position Posició - + Version de QElectroTech Versió de QElectroTech - + Numéro de folio Número de full - + Position du folio Posició du full - + Nombre de folio Nombre de full - + Numéro du folio précédent Número de full anterior - + Numéro du folio suivant Número de full següent - + Titre du projet Títol del projecte - + Chemin du fichier du projet Ruta del fitxer del projecte - + Nom du fichier Nom del fitxer - + Date d'enregistrement du fichier format local Data de desament del fitxer en format local - + Date d'enregistrement du fichier format dd-MM-yyyy Data d'enregistrament del fitxer en format dd-MM-aaaa - + Date d'enregistrement du fichier format yyyy-MM-dd Data d'enregistrament del fitxer en format aaaa-MM-dd - + Heure d'enregistrement du fichier Temps de gravació del fitxer - + Nom du fichier enregistré Nom del fitxer desat - + Chemin du fichier enregistré Ruta del fitxer desat - + Formule du label Fórmula de l'etiqueta - + Label Etiqueta Table PLC - + Taula de PLC Table PLC (vide) - + Taula PLC (buida) - + - + Type - Tipus + Tipus - + - + Adresse - + Adreça - + - - + + Commentaire Comentari - + - - + Réf. croisée - + Referència creuada - + - - - + + + Fonction Funció - + Bloc auxiliaire 1 Bloc auxiliar 1 - + Description textuelle auxiliaire 1 Descripció textual auxiliar 1 - + Numéro d'article auxiliaire 1 Número d'article auxiliar 1 - + Fabricant auxiliaire 1 Fabricant auxiliar 1 - + Numéro de commande auxiliaire 1 Número de comanda auxiliar 1 - + Numéro interne auxiliaire 1 Número intern auxiliar 1 - + Fournisseur auxiliaire 1 Proveïdor auxiliar 1 - + Quantité auxiliaire 1 Quantitat auxiliar 1 - + Unité auxiliaire 1 Unitat auxiliar 1 - + Bloc auxiliaire 2 Bloc auxiliar 2 - + Description textuelle auxiliaire 2 Descripció textual auxiliar 2 - + Numéro d'article auxiliaire 2 Número d'article auxiliar 2 - + Fabricant auxiliaire 2 Fabricant auxiliar 2 - + Numéro de commande auxiliaire 2 Número de comanda auxiliar 2 - + Numéro interne auxiliaire 2 Número intern auxiliar 2 - + Fournisseur auxiliaire 2 Proveïdor auxiliar 2 - + Quantité auxiliaire 2 Quantitat auxiliar 2 - + Unité auxiliaire 2 Unitat auxiliar 2 - + Bloc auxiliaire 3 Bloc auxiliar 3 - + Description textuelle auxiliaire 3 Descripció textual auxiliar 3 - + Numéro d'article auxiliaire 3 Número d'article auxiliar 3 - + Fabricant auxiliaire 3 Fabricant auxiliar 3 - + Numéro de commande auxiliaire 3 Número de comanda auxiliar 3 - + Numéro interne auxiliaire 3 Número intern auxiliar 3 - + Fournisseur auxiliaire 3 Proveïdor auxiliar 3 - + Quantité auxiliaire 3 Quantitat auxiliar 3 - + Unité auxiliaire 3 Unitat auxiliar 3 - + Bloc auxiliaire 4 Bloc auxiliar 4 - + Description textuelle auxiliaire 4 Descripció textual auxiliar 4 - + Numéro d'article auxiliaire 4 Número d'article auxiliar 4 - + Fabricant auxiliaire 4 Fabricant auxiliar 4 - + Numéro de commande auxiliaire 4 Número de comande auxiliar 4 - + Numéro interne auxiliaire 4 Número intern auxiliar 4 - + Fournisseur auxiliaire 4 Proveïdor auxiliar 4 - + Quantité auxiliaire 4 Quantitat auxiliar 4 - + Unité auxiliaire 4 Unitat auxiliar 4 - + Type PLC - + Tipus de PLC - + Adresse PLC - + Adreça de PLC - + Fonction PLC - + Funció de PLC - + Commentaire PLC - + Comentari de PLC - + Réf. croisée PLC - + Referència creuada de PLC - + Description textuelle Descripció textual - - Modèle - - - - - Catégorie - - - - - Tension nominale - - - - - Courant nominal - - - - - Notes - - - - + Numéro d'article Número d'article - + Fabricant Fabricant - + Numéro de commande Número de comanda - + Numéro interne Número intern - + Fournisseur Proveïdor - + Quantité Quantitat - + Unité Unitat - + Tension / Protocole Tensió / Protocol - + Couleur du fil Color del fil - + Section du fil Secció de fil - + Formule du texte Fórmula de text @@ -11069,7 +10941,7 @@ Què voleu fer? Compilació : - + Compilation : Compilació: @@ -11215,32 +11087,32 @@ Què voleu fer? Entrée digitale - + Entrada digital Sortie digitale - + Sortida digital Entrée analogique - + Entrada analògica Sortie analogique - + Sortida analògica Entrée universelle - + Entrada universal Sortie universelle - + Sortida universal @@ -11248,15 +11120,15 @@ Què voleu fer? %p% fet (%v de %m) - - - + + + this is an error in the code Això és un error al codi @@ -11267,7 +11139,7 @@ Què voleu fer? Camp de text dinàmic - + Importer un fichier dxf Importa un fitxer dxf @@ -11331,22 +11203,22 @@ Voleu substituir-la? Importa la configuració de text : %1 - + To install the plugin qet_tb_generator<br>Visit :<br><a href='https://pypi.python.org/pypi/qet-tb-generator'>qet-tb-generator</a><br>Requires python 3.5 or above.<br><B><U> First install on Windows</B></U><br>1. Install, if required, python 3.5 or above<br> Visit :<br><a href='https://www.python.org/downloads/'>python.org</a><br>2. pip install qet_tb_generator<br><B><U> Update on Windows</B></U><br>python -m pip install --upgrade qet_tb_generator<br>>>user could launch in a terminal this script in this directory<br> C:\users\XXXX\AppData\Local\Programs\Python\Python36-32\Scripts <br> Per instal·lar el connector qet_tb_generator<br>Visiteu:<br><a href='https://pypi.python.org/pypi/qet-tb-generator'>qet-tb-generator</a><br>Requereix python 3.5 o superior.<br><B><U> Primera instal·lació a Windows</B></U><br>1. Instal·leu, si cal, python 3.5 o superior<br> Visiteu:<br><a href='https://www.python.org/downloads/'>python.org</a><br>2. pip install qet_tb_generator<br><B><U> Actualització a Windows</B></U><br>python -m pip install --upgrade qet_tb_generator<br>>>L'usuari podria iniciar aquest script en un terminal en aquest directori<br> C:\users\XXXX\AppData\Local\Programs\Python\Python36-32\Scripts <br> - + To install the plugin qet_tb_generator<br>Visit :<br><a href='https://pypi.python.org/pypi/qet-tb-generator'>qet-tb-generator</a><br><B><U> First install on macOSX</B></U><br>1. Install, if required, python 3.11 bundle only, <a href='https://www.python.org/ftp/python/3.11.2/python-3.11.2-macos11.pkg'>python-3.11.2-macos11.pkg</a><br>2 Run Profile.command script<br>because program use hardcoded PATH for localise qet-tb-generator plugin <br> Visit :<br><a href='https://qelectrotech.org/forum/viewtopic.php?pid=5674#p5674'>howto</a><br>2. pip3 install qet_tb_generator<br><B><U> Update on macOSX</B></U><br> pip3 install --upgrade qet_tb_generator<br> Per instal·lar el connector qet_tb_generator<br>Visiteu:<br><a href='https://pypi.python.org/pypi/qet-tb-generator'>qet-tb-generator</a><br><B><U> Primera instal·lació a macOSX</B></U><br>1. Instal·leu, si cal, només el paquet python 3.11, <a href='https://www.python.org/ftp/python/3.11.2/python-3.11.2-macos11.pkg'>python-3.11.2-macos11.pkg</a><br>2 Executeu l'script Profile.command<br>perquè el programa utilitza el PATH codificat per localitzar el connector qet-tb-generator <br> Visiteu:<br><a href='https://qelectrotech.org/forum/viewtopic.php?pid=5674#p5674'>com fer-ho</a><br>2. instal·lació de qet_tb_generator amb pip3<br><B><U> Actualització a macOSX</B></U><br> instal·lació de pip3 --upgrade qet_tb_generator<br> - + To install the plugin qet_tb_generator<br>Visit :<br><a href='https://pypi.python.org/pypi/qet-tb-generator'>qet-tb-generator</a><br><br>Requires python 3.5 or above.<br><br><B><U> First install on Linux</B></U><br>1. check you have pip3 installed: pip3 --version<br>If not install with: sudo apt-get install python3-pip<br>2. Install the program: sudo pip3 install qet_tb_generator<br>3. Run the program: qet_tb_generator<br><br><B><U> Update on Linux</B></U><br>sudo pip3 install --upgrade qet_tb_generator<br> Per instal·lar el connector qet_tb_generator<br>Visiteu:<br><a href='https://pypi.python.org/pypi/qet-tb-generator'>qet-tb-generator</a><br><br>Requereix python 3.5 o superior.<br><br><B><U> Primera instal·lació a Linux</B></U><br>1. comproveu que teniu pip3 instal·lat: pip3 --version<br>Si no, instal·leu-lo amb: sudo apt-get install python3-pip<br>2. Instal·leu el programa: sudo pip3 install qet_tb_generator<br>3. Executeu el programa: qet_tb_generator<br><br><B><U> Actualització a Linux</B></U><br>sudo pip3 install --upgrade qet_tb_generator<br> - + Error launching qet_tb_generator plugin Error en iniciar el connector qet_tb_generator @@ -11408,7 +11280,7 @@ Voleu substituir-la? Modifier les proriétés d'un groupe de bornes - Modifica les propietats d'un grup de borns + Edita les propietats d'un grup de borns @@ -11454,7 +11326,7 @@ Voleu substituir-la? Modifier les propriétés d'un élement - Modifica les propietats d'un element + Edita les propietats d'un element @@ -11464,7 +11336,7 @@ Voleu substituir-la? Pivoter le groupe - + Gira el grup @@ -11554,66 +11426,66 @@ Si us plau, descarregueu-lo des de l'enllaç següent i descomprimiu-lo a l Automatic terminal numbering - + Numeració automàtica de borns table PLC element part name - + Taula de PLC Ajouter un folio undo command text - Afegeix un full + Afegeix un full Déplacer un folio undo command text - + Mou un full Supprimer un folio undo command text - + Suprimeix un full Sélectionner un fichier PDF... - + Seleccioneu un fitxer PDF... Fichiers PDF (*.pdf) - + Fitxers PDF (*.pdf) Impossible de charger le fichier PDF. - + No s'ha pogut carregar el fitxer PDF. Le fichier PDF ne contient aucune page. - + El fitxer PDF no conté pàgines. Impossible de déterminer la taille de la page PDF. - + No s'ha pogut determinar la mida de la pàgina del PDF. Impossible de rendre la page PDF. - + No s'ha pogut renderitzar la pàgina del PDF. Transformer %1 - + Transforma %1 @@ -11634,39 +11506,39 @@ Si us plau, descarregueu-lo des de l'enllaç següent i descomprimiu-lo a l QetGraphicsTableItem - + Les information à afficher sont supérieurs à la quantité maximal pouvant être affiché par les tableaux. Veuillez ajouter un nouveau tableau ou regler les tableaux existant afin d'afficher l'integralité des informations. La informació que es mostrarà supera la quantitat màxima que poden mostrar les taules. Afegiu una taula nova o ajusteu les taules existents per mostrar la quantitat completa d'informació. - + Les information à afficher sont supérieurs à la quantité maximal pouvant être affiché par le tableau. Veuillez ajouter un nouveau tableau ou regler le tableau existant afin d'afficher l'integralité des informations. La informació que es mostrarà supera la quantitat màxima que es pot mostrar a la taula. Afegiu una taula nova o ajusteu la taula existent per mostrar la quantitat completa d'informació. - + Limitation de tableau Limitació de la taula - + Modifier la géometrie d'un tableau - Modifica la geometria d'una taula + Canvia la geometria d'una taula QetShapeItem - + une shape una forma - + une ligne una línia @@ -11684,323 +11556,282 @@ Afegiu una taula nova o ajusteu la taula existent per mostrar la quantitat compl Déformer une courbe - + Deformar una corba Nœud le plus proche - + Node més proper Lisse - + Suau Symétrique - + Simètric Anguleux - + Angular Supprimer le nœud le plus proche - + Suprimeix el node més proper - + Convertir en polyligne - + Converteix en polilínia + + + + Miroir horizontal + Mirall horitzontal + + + + Miroir vertical + Mirall vertical - Convertir en courbe de Bézier - + Propriétés... + Propietats... - - Miroir horizontal - + + Taille + Mida - - Miroir vertical - + + Coins arrondis + Cantonades arrodonides + + + + Édition des nœuds + Edició de nodes + + + + Rotation/Inclinaison + Gir/Inclinació - Taille - Mida - - - - Coins arrondis - - - - - Édition des nœuds - - - - - Rotation/Inclinaison - - - - Cliquer : mode %1 - + Clic: mode %1 - + Glisser un coin/bord : redimensionner (Ctrl = depuis le centre, Maj = proportions, Alt = détacher en polyligne) - + Arrossega una cantonada o una aresta: canvia la mida (Ctrl = des del centre, Shift = manté les proporcions, Alt = separa com a polilínia) - + ; point turquoise : arc - + ; punt turquesa: arc - + Glisser une extrémité : la déplacer - + Fes lliscar un extrem: el mou + + + + Glisser un point : le déplacer + Desplaça un punt: el mou + + + + Glisser le point violet : arrondir les coins + Arrossega el punt morat: arrodonix les cantonades + + + + Glisser une poignée ou la courbe : déformer (Alt = briser la tangente) ; Alt+glisser un point anguleux : créer des poignées ; clic droit : menu du nœud le plus proche + Arrossegueu un tirador o la corba: deformar (Alt = trencar la tangent); Alt + arrossegar un punt de cantonada: crear tiradors; clic amb el botó dret: menú del node més proper + + + + une extrémité + un extrem - Glisser un point : le déplacer - + un coin + una cantonada + + + + un point + un punt + + + + Glisser %1 : rotation (Maj = 15°) + Arrossega %1: gira (Maj = 15°) + + + + ; un bord : inclinaison + ; un avantatge: inclinació - Glisser le point violet : arrondir les coins - - - - - Glisser une poignée ou la courbe : déformer (Alt = briser la tangente) ; Alt+glisser un point anguleux : créer des poignées ; clic droit : menu du nœud le plus proche - - - - - une extrémité - - - - - un coin - - - - - un point - - - - - Glisser %1 : rotation (Maj = 15°) - - - - - ; un bord : inclinaison - - - - ; point rouge : glisser pour repositionner le centre de rotation - + ; punt vermell: arrossegueu-lo per reposicionar el centre de rotació - + (Ctrl pendant le glissement = position libre, sans accrochage à la grille) - + (Ctrl mentre s'arrossega = posicionament lliure, sense ajustar-se a la quadrícula) - + — Cliquer : mode %1 - + — Clic: mode %1 + + + + Glisser : déplacer ce point + Arrossega: mou aquest punt + + + + Glisser : redimensionner (Ctrl = depuis le centre + position libre, Maj = proportions + Arrossega: canvia la mida (Ctrl = des del centre + posicionament lliure, Maj = manté les proporcions) + + + + , Alt = détacher en polyligne + , Alt = desconnecta com a polilínia + + + + Glisser : rotation (Ctrl = position libre, Maj = 15°) + Arrossega: gira (Ctrl = posicionament lliure, Maj = 15°) + + + + Glisser : inclinaison (Ctrl = position libre, Maj = 15°) + Arrossega: inclina (Ctrl = posició lliure, Shift = 15°) + + + + Glisser : repositionner le centre de rotation (Ctrl = position libre) + Arrossega: reposiciona el centre de rotació (Ctrl = posicionament lliure) + + + + Glisser : arrondir les coins (Ctrl = position libre) + Arrossega: arrodonir les cantonades (Ctrl = posicionament lliure) + + + + Glisser : ajuster l'arc (Ctrl = position libre, Maj = 15°) + Arrossega: ajusta l'arc (Ctrl = posicionament lliure, Maj = 15°) + + + + Glisser : déplacer le point (Ctrl = position libre + Arrossega: mou el punt (Ctrl = posició lliure) - Glisser : déplacer ce point - - - - - Glisser : redimensionner (Ctrl = depuis le centre + position libre, Maj = proportions - - - - - , Alt = détacher en polyligne - - - - - Glisser : rotation (Ctrl = position libre, Maj = 15°) - - - - - Glisser : inclinaison (Ctrl = position libre, Maj = 15°) - - - - - Glisser : repositionner le centre de rotation (Ctrl = position libre) - - - - - Glisser : arrondir les coins (Ctrl = position libre) - - - - - Glisser : ajuster l'arc (Ctrl = position libre, Maj = 15°) - - - - - Glisser : déplacer le point (Ctrl = position libre - - - - , Alt = créer des poignées - + , Alt = crear tiradors - + Glisser : déformer la courbe (Ctrl = position libre, Alt = briser la tangente) - + Lliscament: deforma la corba (Ctrl = posició lliure, Alt = trenca la tangent) - + Ajouter un point à un polygone Afegeix un punt a un polígon - + Supprimer un point d'un polygone Elimina un punt d'un polígon - + Ajouter un point à une courbe - + Afegeix un punt a una corba - + Supprimer un point d'une courbe - + Suprimeix un punt d'una corba - - Convertir %1 en courbe de Bézier - - - - + Convertir %1 en polyligne - + Converteix %1 en una polilínia - + Miroir impossible : inclinaison trop extrême pour cette forme - + Mirall impossible: inclinació excessiva per a aquesta forma - + Miroir horizontal de %1 - + Mirall horitzontal de %1 - + Miroir vertical de %1 - + Mirall vertical de %1 - + Modifier le type d'un nœud - + Canvia el tipus d'un node - - Redimensionner %1 - - - - - Faire pivoter %1 - - - - - Incliner %1 - - - - + Deplacer le centre de rotation - + Mou el centre de rotació - - Arrondir les coins d'%1 - - - - - Modifier l'angle d'un arc - - - - - - Modifier la forme d'%1 - - - - - Modifier la courbure d'%1 - - - - + Modifier %1 Modifica %1 - + un rectangle un rectangle - + une éllipse una el·lipse - + un arc - + un arc - + une polyligne un polígon - + une courbe - + una corba @@ -12997,29 +12828,29 @@ Les autres champs ne sont pas utilisés. Géométrie - + Geometria Largeur - Amplada + Amplada Hauteur - Alçada + Alçada Angle - + Angle ° - + ° @@ -13104,7 +12935,7 @@ Les autres champs ne sont pas utilisés. Forme fermée - + Forma tancada @@ -13114,27 +12945,27 @@ Les autres champs ne sont pas utilisés. Modifier la longueur d'une ligne - + Canvia la longitud d'una línia Modifier la taille d'une forme - + Canviar la mida d'una forma Modifier l'angle d'une forme - + Canvia l'angle d'una forma Modifier le trait d'une forme - Modifica el traç d'una forma + Canvia el traç d'una forma Modifier le remplissage d'une forme - Modifica l'emplenament d'una forma + Canvia l'emplenament d'una forma @@ -13155,17 +12986,17 @@ Les autres champs ne sont pas utilisés. Longueur - + Longitud Rayon X - + Eix X Rayon Y - + Eix Y @@ -13173,66 +13004,66 @@ Les autres champs ne sont pas utilisés. Filtrer les raccourcis… - + Filtra les dreceres... Tous - Tots + Tots Attribués uniquement - + Només els assignats Non attribués uniquement - + Només els no assignats Conflits uniquement - + Només els conflictius Action - + Acció Raccourci - + Drecera Tout réinitialiser - + Restablir-ho tot Réinitialiser ce raccourci - + Restableix aquesta drecera %n action(s) - - - + + %n acció + %n accions - + Ce raccourci est aussi utilisé par : %1 - + Aquesta drecera també la utilitza: %1 - + Raccourcis configuration page title - + Dreceres @@ -15265,7 +15096,7 @@ Les autres champs ne sont pas utilisés. T%1 - + T%1 @@ -15305,91 +15136,91 @@ Les autres champs ne sont pas utilisés. NO (contact SW) - + Normalment Obert en contacte commutat (NO) NC (contact SW) - + Normalment Tancat en contacte commutat (NC) Commun (contact SW) - + Comú en contacte commutat Modifier l'orientation d'une borne - Modifica l'orientació d'un born + Canvia l'orientació d'un born Modifier le nom du terminal - Modifica el nom del born + Edita el nom del born Modifier le type d'une borne - Modifica el tipus d'un born + Canvia el tipus d'un born Afficher/cacher le nom du terminal - + Mostra/amaga el nom del born Modifier la position du label - + Canvia la posició de l'etiqueta Modifier la police du label - + Canvia el tipus de lletra de l'etiqueta Modifier la taille de police du label - + Canvia la mida de la lletra de l'etiqueta Modifier la rotation du label - + Canvia la rotació de l'etiqueta Modifier l'alignement du label - + Canvia l'alineació de l'etiqueta Afficher/cacher le cadre du label - + Mostra/amaga el marc de l'etiqueta Modifier la couleur du label - + Canvia el color de l'etiqueta Modifier l'étiquette du maître - + Edita l'etiqueta del mestre Modifier le nom de la borne - + Edita el nom del born Modifier l'index de l'étiquette du maître - + Canvia l'índex de l'etiqueta del mestre @@ -15429,67 +15260,67 @@ Les autres champs ne sont pas utilisés. Nom de la borne - + Nom del born Afficher le nom - + Mostrar el nom Propriétés du texte - + Propietats del text Police - Font + Font X : - X : + X : Y : - Y : + Y : Rotation : - Rotació : + Rotació : ° - ° + ° Alignement - Alineació + Alineació Couleur : - Color : + Color : Encadrer le texte - Emmarca el text + Emmarca el text Étiquette du maître - + Etiqueta mestra Reprendre du maître - + Reiniciar el mestre @@ -15497,57 +15328,57 @@ Les autres champs ne sont pas utilisés. Numérotation automatique des bornes - + Numeració automàtica de borns Cette fonction numérote les bornes du projet selon leur position. Les bornes vides ou verrouillées sont ignorées.Le marquage des bornes doit être configuré au préalable comme suit : '-X:AB'. La partie avant les deux-points (le bornier) peut être nommée au choix. 'AB' peut être composé de chiffres ou de lettres." - + Aquesta funció numera els borns del projecte segons la seva posició. Els borns buits o bloquejats s'ignoren. L'etiquetatge dels borns s'ha de configurar prèviament de la manera següent: «-X:AB». La part anterior als dos punts (el regle de borns) es pot anomenar com es vulgui. «AB» pot constar de números o lletres." Priorité des axes - + Prioritat dels eixos Priorité à l'axe X (horizontal) - + Prioritat a l'eix X (horitzontal) Priorité à l'axe Y (vertical) - + Prioritat a l'eix Y (vertical) Type de numérotation - + Tipus de numeració Numérique uniquement (1, 2, 3...) - + Només numèric (1, 2, 3...) Alphanumérique (A, B, C... 1, 2...) - + Alfanumèric (A, B, C... 1, 2...) Numéroter également les lettres - + Numeri també les lletres Borniers - Bloc de borns + Bloc de borns Décochez les borniers dont la numérotation doit être exclue - + Desseleccioneu els blocs de borns la numeració dels quals s'ha d'excloure @@ -15739,7 +15570,7 @@ Les autres champs ne sont pas utilisés. Modifier des propriétés de borniers - Modifica les propietats del bloc de borns + Edita les propietats del bloc de borns @@ -15833,47 +15664,47 @@ Les autres champs ne sont pas utilisés. Orientation - + Orientació Alignement - Alineació + Alineació Police : - Tipus de lletra : + Tipus de lletra : Taille : - Mida : + Mida : Texte d'en tête - + Text de la capçalera Origine vertical - + Origen vertical Longueur maximal - + Longitud màxima Texte borne - + Text del born Référence croisée - + Referència creuada @@ -16025,7 +15856,7 @@ Les autres champs ne sont pas utilisés. Modifier le contenu d'un champ texte - Canvia el contingut d'un camp de text + Edita el contingut d'un camp de text @@ -16092,17 +15923,17 @@ Les autres champs ne sont pas utilisés. Alignement - Alineació + Alineació Point d'ancrage du texte et alignement des lignes entre elles - + Punt d'ancoratge del text i alineació de les línies entre elles Modifier l'alignement d'un champ texte - Canvia l'alineació d'un camp de text + Canvia l'alineació d'un camp de text @@ -16349,13 +16180,13 @@ associar el nom "volta" amb el valor "1745" substituirà %{v TitleBlockTemplate - + %1 : %2 titleblock content - please let the blank space at the beginning %1 : %2 - + %1 %1 @@ -16759,7 +16590,7 @@ associar el nom "volta" amb el valor "1745" substituirà %{v Modifier la largeur de cet aperçu context menu - Modifica l'amplada d'aquesta vista prèvia + Canvia l'amplada d'aquesta vista prèvia @@ -16799,14 +16630,16 @@ associar el nom "volta" amb el valor "1745" substituirà %{v Attention : la somme des largeurs relatives dépasse 100%% de la largeur totale, ce modèle de cartouche ne peut être satisfait par aucune largeur. tooltip warning shown when a template's relative-to-total-length columns alone already exceed 100%% of the total width - + Avís: la suma de les amplades relatives supera el 100 % de l'amplada total; aquest model de cartutx no es pot encabir en cap amplada. + Attention : les colonnes de largeur fixe ne peuvent pas tenir dans la largeur restante, ce modèle de cartouche ne peut être satisfait par aucune largeur. tooltip warning shown when a template's relative-to-total-length columns already consume all available width, leaving no room for its fixed-width columns - + Avís: les columnes d'amplada fixa no caben dins l'amplada restant; aquest model de cartutx no es pot ajustar a cap amplada. + @@ -16821,14 +16654,16 @@ associar el nom "volta" amb el valor "1745" substituirà %{v Longueur maximale : %1px tooltip showing the maximum width of the edited template - + Longitud màxima: %1px + Longueur non contrainte. tooltip shown when the edited template has neither a minimum nor a maximum width constraint - + Longitud no restringida. + @@ -16853,161 +16688,97 @@ associar el nom "volta" amb el valor "1745" substituirà %{v Caixetins del projecte "%1" - - WiringListDialog - - - Liste de câblage - window title - - - - - Fil - column title - - - - - Composant 1 - column title - - - - - Borne 1 - column title - - - - - Composant 2 - column title - - - - - Borne 2 - column title - - - - - Folio - column title - Full - - - - - %n conducteur(s) listé(s). - wiring list summary - - - - - - - - %n conducteur(s) exclu(s) : une extrémité n'est rattachée à aucun élément. - wiring list exclusion warning - - - - - - WiringListExport Erreur - Error + Error Impossible de lire la structure en mémoire du projet. - + No s'ha pogut llegir l'estructura del projecte a la memòria. Exporter le plan de câblage - + Exporta l'esquema de cablejat Fichiers CSV (*.csv) - + Fitxers CSV (*.csv) Impossible d'ouvrir le fichier pour l'écriture. - + No s'ha pogut obrir el fitxer per a l'escriptura. Page Wiring list CSV header - + Pàgina Composant 1 Wiring list CSV header - + Component 1 Borne 1 Wiring list CSV header - + Born 1 Composant 2 Wiring list CSV header - + Component 2 Borne 2 Wiring list CSV header - + Born 2 Tension / Protocole Wiring list CSV header - Tensió / Protocol + Tensió / Protocol Couleur du fil Wiring list CSV header - Color del fil + Color del fil Section du fil Wiring list CSV header - Secció de fil + Secció de fil Fonction Wiring list CSV header - Funció + Funció Export réussi - + Exportació realitzada amb èxit Le plan de câblage a été exporté avec succès ! - + El pla de cablejat s'ha exportat correctament! @@ -17058,57 +16829,47 @@ associar el nom "volta" amb el valor "1745" substituirà %{v Posició de l'esclau de la referència creuada - - Distance label - slave : - - - - - Distance in pixels between the label and the slave cross reference - - - - + Afficher les numéros de bornes dans les Xrefs - + Mostra els números de born a les referències creuades - + Affiche&r en contacts &Mostra en contactes - + Afficher en croix Mostra en creu - + Label des références croisées Etiqueta de referència creuada - + Maitre Mestre - + %f-%l%c %f-%l%c - + Esclave Esclau - + (%f-%l%c) (%f-%l%c) - + Créer votre propre texte en vous aidant des variables suivantes : %f : le numéro de folio %F: le label de folio @@ -17125,27 +16886,27 @@ associar el nom "volta" amb el valor "1745" substituirà %{v %LM : Ubicació - + Option d'affichage en croix Opció de visualització en creu - + Afficher les contacts de puissance dans la croix Mostra els contactes de potència a la creu - + Préfixe des contacts de puissance : Prefix dels contactes de potència: - + Préfixe des contacts temporisés : Prefix dels contactes temporitzats : - + Préfixe des contacts inverseurs : Prefix dels contactes inversors : @@ -17167,7 +16928,7 @@ associar el nom "volta" amb el valor "1745" substituirà %{v Automate (PLC) - + Autòmat programable (PLC) @@ -17204,21 +16965,16 @@ associar el nom "volta" amb el valor "1745" substituirà %{v Text alignment Alineació del text - - - Champ de texte - - projectDataBase - + Exporter la base de données interne du projet Exporta la base de dades interna del projecte - + sans_nom sense_nom @@ -17279,7 +17035,7 @@ associar el nom "volta" amb el valor "1745" substituirà %{v Éditeur de texte - + Editor de text From 86ce8fcd929868be2fd44fa1c61448a558d02034 Mon Sep 17 00:00:00 2001 From: Laurent Trinques Date: Mon, 14 Sep 2026 13:04:02 +0200 Subject: [PATCH 08/18] git submodule update --remote elements --- elements | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/elements b/elements index 74bdb7784..99c4d51b2 160000 --- a/elements +++ b/elements @@ -1 +1 @@ -Subproject commit 74bdb77844ec5dfa09c7689756f5983197429dce +Subproject commit 99c4d51b2c26e70b3914407df6edebcd0f81f73c From bdd52a0e2bb0ae63fe4c0c61c0f2e144ab13a41c Mon Sep 17 00:00:00 2001 From: Laurent Trinques Date: Mon, 14 Sep 2026 13:06:16 +0200 Subject: [PATCH 09/18] Update ca translations, thanks Antoni --- man/files/ca/man1/qelectrotech.1.gz | Bin 1501 -> 1501 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/man/files/ca/man1/qelectrotech.1.gz b/man/files/ca/man1/qelectrotech.1.gz index 99ea687366398a5e17323c68defcd0ec8c577457..a3a9ffbeba6c3abe1679fd66989262a95d5ccf89 100644 GIT binary patch delta 1434 zcmV;L1!el(3*8F{ABzYGb7rP$kq9UV!de~*^9lsueNYcio_Wmi)6;O&bTVLWND+Y1vju2gu~aY+dB)cL}}*8Y_!Xw6r46_ zDC|`sGGUpJiw|f4iS>tuoo=r`>;&NvtDmLy_oLm>ZlbeHtI;m*WkNYS+D*ZXCWDuw zNoNrCN0Xy}A8NL00@J4~Bv()F&xFjEt2x^IjJx!GE{p}OYK|Zg%8Kg4r1Pp1Uql1) z6^3EpGdqrk?LnvC?ks){*3etrT2?3~o`5D~k;L_KFy*jy_ZOSH~~VX}&UfwNqvcrZ75E)-yJf=SL2EkFH@ z2_&%i3l~|=jf0dky=k95RkR3$E=3^e(w(Oq5h6TEgtZit4~a9UeTs;ccjhBq{K=Fi zPi340Ava-gdOwCL0Pi@*Q#tU;Yr^rqY8MEtTsxNO6ef^x!>pKUW!X%lLuDZqXr^;* z+~Q+@=&Rbpg7&(@AdLG#MUKx>bP$|7kl=pe9t3W3;W)!B!T@99#t69)%f?;F(6Ug- zi>`KjpS7@B7Jca4+bNw`8=LVlT}F9YV74*ixRHy*XXxk-3=i1|bBadgPaY(M zLyZaVa$O$66K-)!9W()VgGH_DMo9h`&&Flc4}9|+JGWc|xywHhXVbF`fSv@LC_W^;l8&@2z?69@U%!aLUq54Yj==llG8J7(S-+fY(znS zGL#s|?;K*U5gOE9HT_w6BE8*V>w;VJixK#b8^U`gZbX(#vCsCx=iBQ5lsdicN8;DT z2bBPS=<1|<-aP4_Zj*@jgBaAbz+6yk2x2EZBYf3XCG#m)89IYMi;vPlR?D}%b@98R z8yniBXtjg*lh95LN8Pmz?y#B$>4CU^p=90qnq)p^L&rgRIj^J>6@wr|`h~#|R!#Gc zBfZcTFgfP~14X3Ul+6ktp-((}N5^B~n{$*Qn$XYUr^6WG8BK z4F}pXA{7%*0&WOY&ncR4_eTSUzEkyq#=IUV1+1vsfE0Abw`nxkDnEbqkmeXz5=t-raIA$RNAQ9jjPV1wZ`PDeo9ST?j!kgE=YV;f6v7x#?BK?) zA2B}Vg~9MxjHXm=c3_b`Ip{}!?7!7^R&dsqeC{K8=hu%Iw|_Js!-?UAb-)f1T5d4) zsdl=rdV{lw9XC+NdYCj;2d&1~U0hC!vcYiBazYbs+0W*zdB@p6<^3HJ1V9z z*6uwb{ivsLF?FR)^U%L9<^s_seJFby1rB@rUHQPl&@oCEGE7`)WQW4C6~v{owFQyM z813IahR;h0khMUT56eLm4-rQsPY<sLXRfW&9Qc>H~GQHDO+nnZdwEH653ui8qB_qnYx6-xp0ah-s zjZt|KAbQ>U7hF+$c>yT{AoMUUz@Ujgh>RfVKtzxX)IsDB*f8o2Bi3&9*-2~A>c*W` oh0UAjd=Rm?b#}D)C3q{wOixG(Wi7D>RXhy-1w*J{U;PXK03s;9IRF3v delta 1434 zcmV;L1!el(3*8F{ABzYG+v){rkq9UVW2>+wRaz$jkvKAcr|i4vENaJtUL3Vg*>kql zI_V8#_F`}Et1aA~xNHQL3!OekkZyD6B_Wbkq{ z=?tR&Xma#_L(NuAVEUAWIR7k@IP z$x|68LC8%QoZXM13cx$g@l+1H@|tkGui6DdE7y)?I)w=&+%PMqT3I&J=ulZm1)Awx z8@Kp>82YOAu%Nx}FbLy*P_g5)932D~4kWmrxCeooTsY2fi!i{LxG_R*#IkW$GPEod z@}jF9-)AkXmPH>r_jXEWlqr5%{KjT{OqWrf7MN|!IBw)3@fkY01H(f$!klAU$Cs_M zv)-G_-s@;E=p09v?dbTj*N-ttmKKI9S4Ne8#AF z>g9b;)TU1P|Diztp*|Z0ZxFPFNx1K=5<;JXGdwMljZob$Ix5+cheAgc^Em0NIJ! zT*HC3j7Y@!<_%JP=@G`b!6tLx_F;{=lx(#D%r#U8w+$SjxwGy}+8mjz@F^+McH6DXgQ&Yz!RY*b>sLXRfSKp5>eaKGQE>j+nnZdwEH653ui8qB_YbOx01E;{Z%fo zi&1zHA9~#S2V7BmcmW9mAo4ISz@Ujgh=d^JKtPZS)Ij7A*f8o2Bi3&9*-2~A>c*W` o1MLZ1t1-9wkGyMzz0L1*tvH$=8 From 9363e9bc2ecbac49cf33680e700ee15498ffb84f Mon Sep 17 00:00:00 2001 From: ispyisail Date: Tue, 15 Sep 2026 07:09:16 +1200 Subject: [PATCH 10/18] Add a regression test for the forwarded-file use-after-free Covers the crash fixed in #868: a file forwarded from a second instance was opened inside SingleApplication's socket handler, so the backup prompt's nested event loop ran while that handler was still on the stack. Run by hand; no build system or CI changes. tests/ipc-regression/run.sh --binary build/qelectrotech Validated in both directions on Qt 6.10.2: ceda1e082 (before the fix) crashes 3 times in 3 with exit 139, 199444b6 (after) survives 3 times in 3. Three requirements are not obvious and are documented in the script: - Qt 6 only. An unfixed Qt 5 build survives every attempt, so the script refuses to run on a Qt 5 binary rather than report a pass that cannot fail. - A Debug build. The same unfixed commit survives every attempt built -O3 -DNDEBUG; whether a use-after-free faults depends on what the allocator does with the freed block. - Dismissing the backup prompt is the step that triggers it. Left open, the stack never unwinds and nothing fails, which is why the bug was twice reported as not reproducible. The test runs in its own sandbox on its own X display, works on a copy of the project so backup files do not land in examples/, and cleans up after itself. Co-Authored-By: Claude Opus 5 --- tests/ipc-regression/README.md | 73 +++++++ tests/ipc-regression/run.sh | 356 +++++++++++++++++++++++++++++++++ 2 files changed, 429 insertions(+) create mode 100644 tests/ipc-regression/README.md create mode 100755 tests/ipc-regression/run.sh diff --git a/tests/ipc-regression/README.md b/tests/ipc-regression/README.md new file mode 100644 index 000000000..9208723d7 --- /dev/null +++ b/tests/ipc-regression/README.md @@ -0,0 +1,73 @@ +# IPC open-forwarding regression test + +Guards the use-after-free fixed in PR #868. + +With QElectroTech already running, opening a `.qet` from a file manager forwards +the path to the running instance over SingleApplication's local socket. Before +#868, `QETApp::receiveMessage()` called `openFiles()` directly — inside the +socket's `readyRead` handler. Loading the project raises a modal backup prompt +whose `exec()` runs a nested event loop while that handler is still on the +stack. The second instance then exits, its socket is deleted, and when the +prompt is dismissed and the stack unwinds, `QMetaObject::activate()` carries on +emitting on freed memory. + +## Running it + +```bash +tests/ipc-regression/run.sh --binary build/qelectrotech +``` + +Needs `Xvfb`, `openbox` and `xdotool`. It allocates its own display, runs +entirely in a sandbox, and cleans up after itself. + +Exit codes: `0` survived, `1` crashed, `2` inconclusive or unusable input. + +## Requirements that are not obvious + +**Qt 6.** An unfixed Qt 5 build survives this scenario every time — measured, +not assumed. The script refuses to run on a Qt 5 binary rather than report a +pass that cannot fail. Reproduced on Qt 6.10.2. + +**A Debug build.** Whether a use-after-free faults depends on what the allocator +does with the freed block. The same unfixed commit crashes 3 times in 3 built +Debug, and survives every attempt built `-O3 -DNDEBUG`. + +**A large project.** The load has to take long enough that the prompt is up +while the socket handler is still on the stack. `examples/industrial.qet` +(2.6 MB, ~4 s) is used by default. + +## Why it repeats + +The crash is a race: the second instance's disconnect has to be processed while +the nested event loop is on the stack. Single attempts reproduced it 2 times in +3, so one attempt would let a reintroduced bug through about a third of the +time. Three attempts is roughly 96%; `QET_IPC_ATTEMPTS` changes it. Any single +crash fails the run, and survival must be unanimous. + +## Validation + +| build | result | +|---|---| +| `ceda1e082` (before #868) | 3/3 crashed, exit 139 | +| `199444b6` (after #868) | 3/3 survived | + +If this test is changed, re-validate it against a build without the fix. A gate +nobody has watched go red is not evidence. + +## Things that made earlier versions of this test pass a crashing build + +Recorded because each one looked exactly like success. + +**Answering the wrong dialog.** A progress dialog appears several seconds before +the backup prompt and both stay up. Dismissing the progress dialog satisfies a +naive "a modal appeared, dismiss it" check while never touching the crash path. + +**Filtering that dialog out by title.** That is a locale filter: it worked in +French and silently stopped working in English. The script now dismisses every +dialog and identifies none, and treats a run that saw fewer than two distinct +dialogs as inconclusive rather than passing. + +**Sharing a home directory between attempts.** A crashed attempt leaves a backup +file, so the next one opens with the restore prompt instead of the backup +prompt — a path that never unwinds the socket stack. Each attempt now gets a +fresh `HOME`. diff --git a/tests/ipc-regression/run.sh b/tests/ipc-regression/run.sh new file mode 100755 index 000000000..fec2ee98d --- /dev/null +++ b/tests/ipc-regression/run.sh @@ -0,0 +1,356 @@ +#!/usr/bin/env bash +# +# IPC open-forwarding regression gate -- upstream PR #868. +# +# tests/ipc-regression/run.sh --binary build-fast/qelectrotech +# +# WHAT IT GUARDS +# +# With QET already running, opening a .qet from a file manager forwards the +# path to the running instance over SingleApplication's local socket. Before +# #868, QETApp::receiveMessage() called openFiles() directly -- inside the +# socket's readyRead handler. openFiles() loads the project (seconds, on a +# large one) and puts up a modal BackupDialog whose exec() runs a NESTED event +# loop while the socket handler is still on the stack. During that nested loop +# the secondary exits, the connection closes, the QLocalSocket is deleted, and +# when the dialog is dismissed and the stack unwinds QMetaObject::activate() +# carries on emitting on the freed sender. Segfault. +# +# #868 defers openFiles() with a zero-timer so the socket stack unwinds first. +# The comment there is long on purpose; this test is the other half of making +# sure nobody "simplifies" it away. +# +# THE STEP EVERYONE MISSES +# +# Leaving the backup dialog OPEN never crashes -- the stack never unwinds. +# The bug was twice reported unreproducible for exactly this reason. DISMISSING +# the dialog is the load-bearing step, not opening the project. That is why +# this test needs a window manager and synthetic input rather than being a +# headless CLI check. +# +# WHY A COPIED BINARY (the isolation that makes this safe to run) +# +# SingleApplication hashes the socket name from app/org name, version, and -- +# on Linux, unless ExcludeAppPath is set -- applicationFilePath() +# (SingleApplication/singleapplication_p.cpp:156-171). QET constructs it as +# `SingleApplication app(argc, argv, true)` (sources/main.cpp:160), so the +# path IS in the hash. +# +# That matters twice over: +# * Qt's local sockets live in the abstract namespace, which is scoped to the +# network namespace, so a container sharing the host's network namespace +# and running the same binary path will silently capture native launches. +# * Copying the binary to a unique path therefore gives this test its OWN +# socket. It cannot talk to, or be hijacked by, a QElectroTech you already +# have open -- and two runs of this test cannot collide with each other. +# +# Do not "optimise" the copy into a symlink: applicationFilePath() resolves +# through /proc/self/exe, so a symlink lands back on the real path and the +# isolation is silently lost. +# +set -uo pipefail + +BINARY="" +PROJECT="" +KEEP_OPEN=0 + +while [ $# -gt 0 ]; do + case "$1" in + --binary) BINARY="$2"; shift 2 ;; + --project) PROJECT="$2"; shift 2 ;; + --keep-open) KEEP_OPEN=1; shift ;; + *) echo "unknown argument: $1" >&2; exit 2 ;; + esac +done + +[ -n "$BINARY" ] || { echo "usage: $0 --binary [--project ]" >&2; exit 2; } +[ -x "$BINARY" ] || { echo "not executable: $BINARY" >&2; exit 2; } +BINARY="$(readlink -f "$BINARY")" + +# A big project is required, not incidental: the load has to take long enough +# that the modal is up while the socket handler is still on the stack. +# industrial.qet is 2.6 MB and loads in ~4 s. +if [ -z "$PROJECT" ]; then + SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + for cand in "$(dirname "$BINARY")/../examples/industrial.qet" \ + "$SCRIPT_DIR/../../examples/industrial.qet"; do + [ -f "$cand" ] && { PROJECT="$(readlink -f "$cand")"; break; } + done +fi +[ -f "$PROJECT" ] || { echo "no project found; pass --project" >&2; exit 2; } + +for tool in Xvfb openbox xdotool; do + command -v "$tool" >/dev/null || { echo "missing required tool: $tool" >&2; exit 2; } +done + +# Qt 5 cannot prove anything here. An unfixed Qt 5 build of master survives +# this scenario every time -- measured, not assumed -- so a Qt 5 run reports +# PASS whether or not the bug is present. Refusing is the only honest answer: +# a green that cannot go red is worse than no test. +# +# Reproduced on Qt 6.10.2, which is also the version in the original report. +# Whether older Qt 6 reproduces it is UNVERIFIED -- if you run this on Qt 6.2 +# or 6.4 and it passes, confirm against a deliberately unfixed build before +# believing it. +if ldd "$BINARY" 2>/dev/null | grep -q "libQt5Core"; then + echo "INCONCLUSIVE: $BINARY links Qt 5." >&2 + echo " This crash only reproduces on Qt 6; an unfixed Qt 5 build" >&2 + echo " survives every attempt. Build against Qt 6 to use this gate." >&2 + exit 2 +fi + +SANDBOX="$(mktemp -d /tmp/qet-ipc-regression.XXXXXX)" +LOG_DIR="$SANDBOX/logs"; mkdir -p "$LOG_DIR" +PRIMARY_PID=""; SECONDARY_PID=""; XVFB_PID=""; OPENBOX_PID="" + +cleanup() { + if [ "$KEEP_OPEN" = "1" ]; then + echo "--keep-open: DISPLAY=$DISPLAY sandbox=$SANDBOX (nothing killed)" + return + fi + for pid in "$SECONDARY_PID" "$PRIMARY_PID" "$OPENBOX_PID" "$XVFB_PID"; do + [ -n "$pid" ] && kill "$pid" 2>/dev/null + done + sleep 0.3 + for pid in "$SECONDARY_PID" "$PRIMARY_PID" "$OPENBOX_PID" "$XVFB_PID"; do + [ -n "$pid" ] && kill -9 "$pid" 2>/dev/null + done + [ "${KEEP_LOGS:-0}" = "1" ] || rm -rf "$SANDBOX" +} +trap cleanup EXIT + +# Unique binary path == private SingleApplication socket. See header. +TEST_BINARY="$SANDBOX/qelectrotech-ipctest" +cp "$BINARY" "$TEST_BINARY" || { echo "could not copy binary" >&2; exit 2; } + +# Work on a COPY of the project, inside the sandbox. +# +# Answering "yes" to the backup prompt is the whole point of this test, and +# QElectroTech writes that backup next to the project it opened. Pointed at +# examples/ directly, a three-attempt run drops three dated .qet files into the +# source tree and leaves them there. Copying first keeps the repository clean +# and means the teardown takes the backups with it. +SANDBOX_PROJECT="$SANDBOX/$(basename "$PROJECT")" +cp "$PROJECT" "$SANDBOX_PROJECT" || { echo "could not copy project" >&2; exit 2; } +PROJECT="$SANDBOX_PROJECT" + +# Isolated HOME so the run cannot inherit or leave behind real settings. +export HOME="$SANDBOX/home" +export XDG_CONFIG_HOME="$HOME/.config" +export XDG_DATA_HOME="$HOME/.local/share" +mkdir -p "$XDG_CONFIG_HOME" "$XDG_DATA_HOME" + +# Xvfb -displayfd atomically picks a free display; hardcoding :99 races. +Xvfb -displayfd 9 -screen 0 1600x1000x24 >"$LOG_DIR/xvfb.log" 2>&1 9>"$SANDBOX/dispnum" & +XVFB_PID=$! +for _ in $(seq 1 50); do + DISP_NUM="$(cat "$SANDBOX/dispnum" 2>/dev/null | tr -d '[:space:]')" + [ -n "$DISP_NUM" ] && break + sleep 0.1 +done +[ -n "${DISP_NUM:-}" ] || { echo "FAIL: Xvfb never reported a display" >&2; exit 1; } +export DISPLAY=":$DISP_NUM" + +# openbox is mandatory -- without a WM, xdotool cannot activate windows +# ("your windowmanager claims not to support _NET_ACTIVE_WINDOW"). +openbox >"$LOG_DIR/openbox.log" 2>&1 & +OPENBOX_PID=$! +sleep 1 + +# Only count real windows: Qt/QPA leaves a HIDDEN 1x1 helper window whose +# title matches the main window exactly, so name-only matching is a coin flip. +real_window() { + local geo w h + geo="$(xdotool getwindowgeometry --shell "$1" 2>/dev/null)" || return 1 + w="$(echo "$geo" | sed -n 's/^WIDTH=//p')" + h="$(echo "$geo" | sed -n 's/^HEIGHT=//p')" + [ -n "$w" ] && [ -n "$h" ] && [ "$w" -gt 50 ] && [ "$h" -gt 50 ] +} + +main_window() { + local w + for w in $(xdotool search --onlyvisible --name "QElectroTech" 2>/dev/null); do + real_window "$w" && { echo "$w"; return 0; } + done + return 1 +} + +# Any real window that is not the main window. NOT filtered by title -- see +# below for why that matters more than it looks. +modal_window() { + local main="$1" w + for w in $(xdotool search --onlyvisible --name "." 2>/dev/null); do + [ "$w" = "$main" ] && continue + real_window "$w" && { echo "$w"; return 0; } + done + return 1 +} + +# Every non-main real window, newest last (X window ids increase). +modal_windows() { + local main="$1" w out="" + for w in $(xdotool search --onlyvisible --name "." 2>/dev/null | sort -n); do + [ "$w" = "$main" ] && continue + real_window "$w" && out="$out $w" + done + echo "$out" +} + +# WHY DIALOGS ARE NOT IDENTIFIED BY TITLE +# +# Opening a project raises a progress dialog several seconds BEFORE the backup +# prompt, and both stay up. Answering the progress dialog looks exactly like a +# successful run: a modal was found, a key was sent, the app survived. PASS, +# against a build that crashes reliably. +# +# The first version of this test filtered the progress dialog out by title. +# That worked locally ("Merci de patienter") and silently broke in Docker, +# where the same dialog is "Thank you for your patience" -- a green that could +# not go red, which is the exact failure this test exists to prevent. A title +# list is a locale list, and there is no reason to believe anyone will keep it +# in sync with QET's translations. +# +# So: dismiss every dialog that appears, repeatedly, and identify nothing. +# The progress dialog ignores Return harmlessly; the backup prompt takes it. +# The run is only meaningful if at least TWO distinct dialogs were seen (the +# progress dialog plus the prompt) -- one means the prompt never appeared and +# the crash path was never exercised, which is INCONCLUSIVE, not a pass. + +ATTEMPTS="${QET_IPC_ATTEMPTS:-3}" + +echo "── QElectroTech IPC open-forwarding regression (PR #868) ──" +echo "binary : $BINARY" +echo "project : $(basename "$PROJECT")" +echo "display : $DISPLAY" +echo "attempts : $ATTEMPTS" +echo + +# WHY THIS REPEATS +# +# The crash is a race: it needs the secondary's disconnect to be processed +# while the modal's nested event loop is still on the stack. Measured on an +# unfixed Qt 6 build of master, a single attempt reproduced it 2 times in 3. +# One attempt is therefore not a gate -- it would wave a reintroduced +# use-after-free through about a third of the time. +# +# Attempts are independent, so N runs miss it with probability (1/3)^N: +# 3 attempts ~= 96% detection, 5 ~= 99.6%. Any single crash fails the run; +# survival must be unanimous. +# +# A fixed build passes every attempt, so the repetition costs nothing but +# time on a green build. + +# Runs the scenario once. 0 = survived, 1 = crashed, 2 = inconclusive. +run_once() { + local attempt="$1" + local primary_pid="" secondary_pid="" main="" modal="" + local log="$LOG_DIR/attempt-$attempt" + mkdir -p "$log" + + # Each attempt gets a FRESH HOME. A crashed attempt leaves a backup file + # behind, and the next run then opens with "Restore file" instead of -- + # or before -- "Create a backup copy?". Observed directly: attempt 2 of a + # Docker run saw a restore prompt and never saw the backup prompt at all, + # so it exercised a different path and proved nothing. Attempts have to be + # independent or the repetition is not buying what it claims to. + export HOME="$SANDBOX/home-$attempt" + export XDG_CONFIG_HOME="$HOME/.config" + export XDG_DATA_HOME="$HOME/.local/share" + rm -rf "$HOME" + mkdir -p "$XDG_CONFIG_HOME" "$XDG_DATA_HOME" + + "$TEST_BINARY" >"$log/primary.log" 2>&1 & + primary_pid=$! + + for _ in $(seq 1 100); do + main="$(main_window)" && [ -n "$main" ] && break + kill -0 "$primary_pid" 2>/dev/null || { echo " primary died during startup"; return 2; } + sleep 0.2 + done + [ -n "$main" ] || { kill "$primary_pid" 2>/dev/null; echo " primary never showed a window"; return 2; } + + for _ in $(seq 1 10); do + m="$(modal_window "$main")" || break + xdotool windowactivate --sync "$m" 2>/dev/null + xdotool key --clearmodifiers Escape 2>/dev/null + sleep 0.3 + done + + "$TEST_BINARY" "$PROJECT" >"$log/secondary.log" 2>&1 & + secondary_pid=$! + + local seen_ids="" distinct=0 crashed=0 + for _ in $(seq 1 200); do + if ! kill -0 "$primary_pid" 2>/dev/null; then crashed=1; break; fi + for w in $(modal_windows "$main"); do + case " $seen_ids " in + *" $w "*) ;; + *) seen_ids="$seen_ids $w" + distinct=$((distinct + 1)) + echo " dialog $distinct: '$(xdotool getwindowname "$w" 2>/dev/null)'" ;; + esac + xdotool windowactivate --sync "$w" 2>/dev/null + xdotool key --clearmodifiers Return 2>/dev/null + done + [ "$distinct" -ge 2 ] && [ -z "$(modal_windows "$main")" ] && break + sleep 0.2 + done + + if [ "$crashed" = "0" ] && [ "$distinct" -lt 2 ]; then + kill "$primary_pid" "$secondary_pid" 2>/dev/null + echo " only $distinct dialog(s) seen -- the backup prompt never appeared," + echo " so the crash path was not exercised" + return 2 + fi + + sleep 2 + local result=0 + if ! kill -0 "$primary_pid" 2>/dev/null; then + wait "$primary_pid" 2>/dev/null; local rc=$? + echo " CRASH: primary died after dismissal (exit $rc)" + result=1 + elif grep -qiE "segmentation fault|SIGSEGV|AddressSanitizer" "$log/primary.log" 2>/dev/null; then + echo " CRASH: primary logged a crash signature" + result=1 + else + echo " survived" + fi + + kill "$primary_pid" "$secondary_pid" 2>/dev/null + sleep 0.5 + kill -9 "$primary_pid" "$secondary_pid" 2>/dev/null + return $result +} + +crashes=0 +inconclusive=0 +for attempt in $(seq 1 "$ATTEMPTS"); do + echo "attempt $attempt/$ATTEMPTS" + run_once "$attempt" + case $? in + 1) crashes=$((crashes + 1)) ;; + 2) inconclusive=$((inconclusive + 1)) ;; + esac +done + +echo +if [ "$crashes" -gt 0 ]; then + echo "FAIL: primary crashed in $crashes of $ATTEMPTS attempts." + echo " This is the PR #868 use-after-free -- forwarded files are being" + echo " opened inside the SingleApplication socket handler again." + echo " Logs: $LOG_DIR (preserved)" + KEEP_LOGS=1 + exit 1 +fi + +if [ "$inconclusive" -eq "$ATTEMPTS" ]; then + echo "INCONCLUSIVE: every attempt failed to exercise the crash path." + echo " Nothing was proven. Check the binary forwards at all (needs" + echo " upstream #861) and that the project is large enough to raise" + echo " the backup prompt." + exit 2 +fi + +echo "PASS: primary survived $((ATTEMPTS - inconclusive)) of $ATTEMPTS attempts, none crashed." +[ "$inconclusive" -gt 0 ] && echo " ($inconclusive attempt(s) inconclusive.)" +exit 0 From 88823ea35ffc7f4b80f85847d3f5a58a4f1c402b Mon Sep 17 00:00:00 2001 From: ispyisail Date: Sat, 1 Aug 2026 00:10:37 +1200 Subject: [PATCH 11/18] Diagram: Tab/Shift+Tab item-selection cycling + select-all-conductors/text-fields (#574) Implements the second pillar of #574: keyboard-driven selection on the diagram canvas. Tab / Shift+Tab select the next / previous item on the current diagram, cycling through items() (z-order) and wrapping at either end. If nothing is selected, Tab selects the first item and Shift+Tab the last. Skipped while a text item has focus, for the same reason arrow-key movement already guards on !focusItem(). Candidates use the same "what counts as a real selectable diagram item" filter (QetGraphicsItem / DiagramTextItem / Conductor) already established by Diagram::invertSelection(), so the cycling order always matches what a user could reach by clicking. Getting Tab to actually reach the scene needed two separate fixes, each independently discovered by empirical testing rather than assumption: - QWidget (DiagramView) intercepts Tab/Backtab for widget focus-chain traversal before generating a key event at all. Overriding DiagramView::focusNextPrevChild() to return false disables that. - QGraphicsScene (Diagram) has its own, separate item-focus-chain traversal, checked before keyPressEvent() is ever reached. The obvious fix -- overriding Diagram::focusNextPrevChild() the same way -- silently does nothing on Qt 5, because QGraphicsScene::focusNextPrevChild() only becomes virtual in Qt 6 (guarded by the QT6_VIRTUAL macro); a compile error surfaced this immediately when attempted directly, rather than shipping a fix that worked on Qt 6 and silently no-opped on Qt 5. Intercepting QEvent::KeyPress in Diagram::event() instead is virtual on every Qt version and sidesteps the scene's internal traversal entirely. Also adds Diagram::selectAllConductors() / selectAllTextFields(), wired up as two new actions in the existing select_all / select_nothing / select_invert action group in qetdiagrameditor.cpp, so they appear in the Edit menu and go through the same QAction -> data() -> selectGroupTriggered() dispatch as the existing selection commands. Verified end-to-end in a real running session (Xvfb + xdotool) with a multi-transistor schematic: Tab/Shift+Tab correctly move a single selection forward/backward through elements and text fields (confirmed via the properties panel updating to each new item and the visual selection box moving on canvas); Tab/Shift+Tab from no selection correctly select the first/last item; "Select all conductors" and "Select all text fields" each correctly select every matching item and deselect everything else. See discussion #574. Co-Authored-By: Claude Sonnet 5 --- sources/diagram.cpp | 107 +++++++++++++++++++++++++++++++++++ sources/diagram.h | 7 ++- sources/diagramview.cpp | 17 ++++++ sources/diagramview.h | 1 + sources/qetdiagrameditor.cpp | 10 ++++ 5 files changed, 141 insertions(+), 1 deletion(-) diff --git a/sources/diagram.cpp b/sources/diagram.cpp index dacc27d4e..47dd2f3e8 100644 --- a/sources/diagram.cpp +++ b/sources/diagram.cpp @@ -450,6 +450,34 @@ void Diagram::wheelEvent(QGraphicsSceneWheelEvent *event) QGraphicsScene::wheelEvent(event); } +/** + @brief Diagram::event + QGraphicsScene has its own Tab/Shift+Tab item-focus-chain traversal + (mirroring QWidget's), checked before keyPressEvent() is ever reached: + by default it would silently consume Tab/Backtab to move focus among + the scene's own focusable items. Intercepting the key press here, + ahead of that, is the only way to reliably override it: unlike + QWidget::focusNextPrevChild(), QGraphicsScene::focusNextPrevChild() is + only virtual starting in Qt 6 (guarded by the QT6_VIRTUAL macro), so a + Diagram:: override of it would silently do nothing on a Qt 5 build. + @param event +*/ +bool Diagram::event(QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + auto *key_event = static_cast(event); + if ((key_event->key() == Qt::Key_Tab || key_event->key() == Qt::Key_Backtab) + && !isReadOnly() && !focusItem()) { + bool forward = key_event->key() == Qt::Key_Tab + && !(key_event->modifiers() & Qt::ShiftModifier); + selectNextItem(forward); + event->accept(); + return true; + } + } + return QGraphicsScene::event(event); +} + /** @brief Diagram::keyPressEvent This event is managed by diagram event interface if any. @@ -1902,6 +1930,85 @@ void Diagram::invertSelection() emit selectionChanged(); } +/** + @brief Diagram::selectAllConductors + Select every conductor on this diagram, deselecting anything else. +*/ +void Diagram::selectAllConductors() +{ + if (items().isEmpty()) return; + + blockSignals(true); + for (auto item : items()) { + item -> setSelected(dynamic_cast(item) != nullptr); + } + blockSignals(false); + emit selectionChanged(); +} + +/** + @brief Diagram::selectAllTextFields + Select every text field on this diagram (independent/static text, + conductor labels, and dynamic element texts), deselecting anything else. +*/ +void Diagram::selectAllTextFields() +{ + if (items().isEmpty()) return; + + blockSignals(true); + for (auto item : items()) { + item -> setSelected(dynamic_cast(item) != nullptr); + } + blockSignals(false); + emit selectionChanged(); +} + +/** + @brief Diagram::selectNextItem + Select the next (or, if @a forward is false, the previous) selectable + item on this diagram, cycling through items() (z-order) and wrapping + around at either end. If nothing is currently selected, selects the + first (or last) item. Uses the same "what counts as a real selectable + diagram item" filter as invertSelection(), so the candidate list and + its order always match what the user could reach by clicking. + @param forward true to select the next item, false for the previous one +*/ +void Diagram::selectNextItem(bool forward) +{ + QList candidates; + for (auto item : items()) { + if (dynamic_cast(item) || + dynamic_cast(item) || + dynamic_cast(item)) { + candidates << item; + } + } + if (candidates.isEmpty()) + return; + + int current_index = -1; + for (int i = 0; i < candidates.size(); ++i) { + if (candidates.at(i) -> isSelected()) { + current_index = i; + break; + } + } + + int next_index; + if (current_index == -1) { + next_index = forward ? 0 : candidates.size() - 1; + } else { + next_index = forward + ? (current_index + 1) % candidates.size() + : (current_index - 1 + candidates.size()) % candidates.size(); + } + + clearSelection(); + QGraphicsItem *next_item = candidates.at(next_index); + next_item -> setSelected(true); + next_item -> ensureVisible(); +} + /** @brief Diagram::insertFolioSeqHash This class inserts a stringlist containing all diff --git a/sources/diagram.h b/sources/diagram.h index 36b346463..34422bfac 100644 --- a/sources/diagram.h +++ b/sources/diagram.h @@ -150,8 +150,11 @@ class Diagram : public QGraphicsScene void wheelEvent (QGraphicsSceneWheelEvent *event) override; void keyPressEvent (QKeyEvent *event) override; void keyReleaseEvent (QKeyEvent *) override; + bool event(QEvent *event) override; + + private: + void selectNextItem(bool forward); - public: void correctTextPos(Element* elmt); void restoreText(Element* elmt); @@ -287,6 +290,8 @@ class Diagram : public QGraphicsScene void selectAll(); void deselectAll(); void invertSelection(); + void selectAllConductors(); + void selectAllTextFields(); signals: void showDiagram (Diagram *); diff --git a/sources/diagramview.cpp b/sources/diagramview.cpp index 4b2d97563..6ee18ca60 100644 --- a/sources/diagramview.cpp +++ b/sources/diagramview.cpp @@ -702,6 +702,23 @@ void DiagramView::focusInEvent(QFocusEvent *e) { } } +/** + @brief DiagramView::focusNextPrevChild + By default, QWidget intercepts Tab/Shift+Tab to move keyboard focus to + the next/previous widget before a key press event is ever generated, + which would silently swallow the diagram's Tab-based item-selection + cycling (see Diagram::event()). Returning false here disables that + automatic focus-chain traversal for this view, so Tab/Shift+Tab reach + keyPressEvent() (and from there, the scene) as ordinary key presses + instead. + @return always false +*/ +bool DiagramView::focusNextPrevChild(bool next) +{ + Q_UNUSED(next) + return false; +} + /** @brief DiagramView::keyPressEvent Handles "key press" events. Reimplemented here to switch to visualisation diff --git a/sources/diagramview.h b/sources/diagramview.h index ae8933660..463ea552c 100644 --- a/sources/diagramview.h +++ b/sources/diagramview.h @@ -80,6 +80,7 @@ class DiagramView : public QGraphicsView void keyPressEvent(QKeyEvent *) override; void keyReleaseEvent(QKeyEvent *) override; bool event(QEvent *) override; + bool focusNextPrevChild(bool next) override; void paintEvent(QPaintEvent *event) override; void mousePressEvent(QMouseEvent *) override; void mouseMoveEvent(QMouseEvent *) override; diff --git a/sources/qetdiagrameditor.cpp b/sources/qetdiagrameditor.cpp index cf6f7d8fd..37745b3c6 100644 --- a/sources/qetdiagrameditor.cpp +++ b/sources/qetdiagrameditor.cpp @@ -688,6 +688,8 @@ void QETDiagramEditor::setUpActions() QAction *select_all = m_select_actions_group.addAction( QET::Icons::EditSelectAll, tr("Tout sélectionner") ); QAction *select_nothing = m_select_actions_group.addAction( QET::Icons::EditSelectNone, tr("Désélectionner tout") ); QAction *select_invert = m_select_actions_group.addAction( QET::Icons::EditSelectInvert, tr("Inverser la sélection") ); + QAction *select_all_conductors = m_select_actions_group.addAction( QET::Icons::Conductor, tr("Sélectionner tous les conducteurs") ); + QAction *select_all_text_fields = m_select_actions_group.addAction( QET::Icons::PartTextField, tr("Sélectionner tous les champs de texte") ); ShortcutManager::instance().registerAction(select_all, "diagrameditor.select_all", tr("Éditeur de schémas"), QKeySequence::SelectAll); ShortcutManager::instance().registerAction(select_nothing, "diagrameditor.select_nothing", tr("Éditeur de schémas"), QKeySequence::Deselect); @@ -696,10 +698,14 @@ void QETDiagramEditor::setUpActions() select_all ->setStatusTip( tr("Sélectionne tous les éléments du folio", "status bar tip") ); select_nothing->setStatusTip( tr("Désélectionne tous les éléments du folio", "status bar tip") ); select_invert ->setStatusTip( tr("Désélectionne les éléments sélectionnés et sélectionne les éléments non sélectionnés", "status bar tip") ); + select_all_conductors ->setStatusTip( tr("Sélectionne tous les conducteurs du folio, désélectionne le reste", "status bar tip") ); + select_all_text_fields->setStatusTip( tr("Sélectionne tous les champs de texte du folio, désélectionne le reste", "status bar tip") ); select_all ->setData("select_all"); select_nothing->setData("deselect"); select_invert ->setData("invert_selection"); + select_all_conductors ->setData("select_all_conductors"); + select_all_text_fields->setData("select_all_text_fields"); connect(&m_select_actions_group, &QActionGroup::triggered, this, &QETDiagramEditor::selectGroupTriggered); @@ -1584,6 +1590,10 @@ void QETDiagramEditor::selectGroupTriggered(QAction *action) diagram->deselectAll(); else if (value == "invert_selection") diagram->invertSelection(); + else if (value == "select_all_conductors") + diagram->selectAllConductors(); + else if (value == "select_all_text_fields") + diagram->selectAllTextFields(); } /** From 22469813fe58120dea93fd06c8b8d14cc420432f Mon Sep 17 00:00:00 2001 From: ispyisail Date: Wed, 9 Sep 2026 19:43:21 +1200 Subject: [PATCH 12/18] Register the two new selection actions with ShortcutManager This branch was cut on 31 July, one day before ShortcutManager landed in 5275fb44f, so the two actions it adds were written before the convention existed and are the only members of the selection group not registered: select_all, select_nothing and select_invert all are. Without this they never appear in the shortcuts configuration page, so a user cannot bind a key to either of them. Registered with an empty default sequence. They are menu actions and neither has an obvious default worth claiming; the point of registering them is that a user can bind one if they want. ShortcutManager stores an empty default without setting a shortcut, and the conflict checker already skips empty sequences. Master merged in first, because ShortcutManager does not exist at this branch's original base. Co-Authored-By: Claude Opus 5 --- sources/qetdiagrameditor.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sources/qetdiagrameditor.cpp b/sources/qetdiagrameditor.cpp index 37745b3c6..51bf8aa32 100644 --- a/sources/qetdiagrameditor.cpp +++ b/sources/qetdiagrameditor.cpp @@ -694,6 +694,10 @@ void QETDiagramEditor::setUpActions() ShortcutManager::instance().registerAction(select_all, "diagrameditor.select_all", tr("Éditeur de schémas"), QKeySequence::SelectAll); ShortcutManager::instance().registerAction(select_nothing, "diagrameditor.select_nothing", tr("Éditeur de schémas"), QKeySequence::Deselect); ShortcutManager::instance().registerAction(select_invert, "diagrameditor.select_invert", tr("Éditeur de schémas"), Qt::CTRL | Qt::Key_I); + //No default sequence for these two: they are menu actions, and the + //point of registering them is so a user can bind one if they want. + ShortcutManager::instance().registerAction(select_all_conductors, "diagrameditor.select_all_conductors", tr("Éditeur de schémas"), QKeySequence()); + ShortcutManager::instance().registerAction(select_all_text_fields, "diagrameditor.select_all_text_fields", tr("Éditeur de schémas"), QKeySequence()); select_all ->setStatusTip( tr("Sélectionne tous les éléments du folio", "status bar tip") ); select_nothing->setStatusTip( tr("Désélectionne tous les éléments du folio", "status bar tip") ); From d9db6e59e79a0d3d879592c822bca4d8c2b33cb4 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Tue, 15 Sep 2026 11:32:42 +1200 Subject: [PATCH 13/18] Let Escape step back out of the folio, so Tab cannot trap keyboard users Tab cycles the folio's items, which means focusNextPrevChild() has to refuse the usual focus traversal. On its own that leaves someone working without a mouse able to reach the drawing area and never leave it -- the exact person the Tab cycling was added for. Escape now steps back out in two stages: it drops the selection first, then hands focus to the next widget. The one-shot m_releasing_focus flag is what lets that second Escape through the override. Verified under Xvfb: with an item selected, Escape clears it (193k pixels change); a second Escape changes nothing visually; a Tab after that moves widget focus in the toolbar (306 pixels) instead of selecting on the canvas, which is the behaviour of a view that no longer holds focus. Co-Authored-By: Claude Opus 5 --- sources/diagramview.cpp | 19 ++++++++++++++++++- sources/diagramview.h | 2 ++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/sources/diagramview.cpp b/sources/diagramview.cpp index 6ee18ca60..ff1c846f3 100644 --- a/sources/diagramview.cpp +++ b/sources/diagramview.cpp @@ -715,7 +715,11 @@ void DiagramView::focusInEvent(QFocusEvent *e) { */ bool DiagramView::focusNextPrevChild(bool next) { - Q_UNUSED(next) + //Escape asked for focus to leave; allow exactly this one traversal. + if (m_releasing_focus) { + m_releasing_focus = false; + return QGraphicsView::focusNextPrevChild(next); + } return false; } @@ -734,6 +738,19 @@ void DiagramView::keyPressEvent(QKeyEvent *e) DiagramContent dc(m_diagram); switch(e -> key()) { + case Qt::Key_Escape: + //Tab cycles the folio's items rather than moving focus (see + //focusNextPrevChild above), so without this there would be no + //way off the canvas for someone working without a mouse. + //Escape steps back out: first it drops the selection, then it + //hands focus to the next widget. + if (m_diagram && !m_diagram->selectedItems().isEmpty()) { + m_diagram->clearSelection(); + } else { + m_releasing_focus = true; + focusNextChild(); + } + return; case Qt::Key_PageUp: current_project->changeTabUp(); return; diff --git a/sources/diagramview.h b/sources/diagramview.h index 463ea552c..0550b01d9 100644 --- a/sources/diagramview.h +++ b/sources/diagramview.h @@ -81,6 +81,8 @@ class DiagramView : public QGraphicsView void keyReleaseEvent(QKeyEvent *) override; bool event(QEvent *) override; bool focusNextPrevChild(bool next) override; + ///Set for one call only, by the Escape handler, to let focus leave the view. + bool m_releasing_focus = false; void paintEvent(QPaintEvent *event) override; void mousePressEvent(QMouseEvent *) override; void mouseMoveEvent(QMouseEvent *) override; From 6a2b3973bcc44e64184ead14208bbdd169bb9c11 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Tue, 15 Sep 2026 11:45:51 +1200 Subject: [PATCH 14/18] Put the drawing tools in a menu so they can be reached without a mouse MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The nine "Ajouter" actions -- text field, image, PDF, line, rectangle, ellipse, polyline, curve, terminal strip -- were only ever added to m_add_item_tool_bar, and the automatic conductor break only to diagram_tool_bar. None carried a shortcut. A toolbar button has no key, so someone working without a mouse could not add anything at all to a folio. They now appear in an "Ajouter" submenu under Édition, and the conductor break beside m_auto_conductor in Projet, the setting it pairs with. The actions themselves are untouched: a QAction can sit in a menu and a toolbar at once, which is what m_depth_action_group -- created a few lines away, and added to both its toolbar and menu_edition -- has always done. That contrast is why this reads as an oversight rather than a decision. Verified by driving the menus with the keyboard alone under Xvfb: Alt+F opens the File menu, Down then Right crosses to Édition, and Right again opens the Ajouter submenu with all eight actions this build compiles (add_pdf is behind QET_HAS_QTPDF and absent on Qt 5). Found with tools/keyboard-audit in the qelectrotech-docker harness, which reports these ten and now reports none. Co-Authored-By: Claude Opus 5 --- sources/qetdiagrameditor.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/sources/qetdiagrameditor.cpp b/sources/qetdiagrameditor.cpp index cf6f7d8fd..48a34c515 100644 --- a/sources/qetdiagrameditor.cpp +++ b/sources/qetdiagrameditor.cpp @@ -934,6 +934,15 @@ void QETDiagramEditor::setUpMenu() menu_edition -> addAction(m_cut); menu_edition -> addAction(m_copy); menu_edition -> addAction(m_paste); + menu_edition -> addSeparator(); + //The same actions the "Ajouter" toolbar holds. They were toolbar-only, + //which left them unreachable for anyone working without a mouse: a + //toolbar button has no key, so text fields, images and every drawing + //shape simply could not be added. m_depth_action_group below has + //always been in both places; this brings these into line with it. + QMenu *menu_add_item = menu_edition -> addMenu(tr("A&jouter")); + menu_add_item -> setIcon(QET::Icons::Add); + menu_add_item -> addActions(m_add_item_actions_group.actions()); menu_edition -> addSeparator(); menu_edition -> addActions(m_select_actions_group.actions()); menu_edition -> addSeparator(); @@ -952,6 +961,9 @@ void QETDiagramEditor::setUpMenu() // menu Projet menu_project -> addAction(m_project_edit_properties); menu_project -> addAction(m_auto_conductor); + //Sits beside m_auto_conductor, the setting it pairs with. It was + //toolbar-only and so had no keyboard route at all. + menu_project -> addAction(m_auto_break_conductor); menu_project -> addSeparator(); menu_project -> addAction(m_project_add_diagram); menu_project -> addAction(m_remove_diagram_from_project); From 6ed26358e8d4436678140f2de481f4f8132f2391 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Tue, 15 Sep 2026 13:03:01 +1200 Subject: [PATCH 15/18] Give the keyboard the folio's context menu, not an item's generic one Pressing the Menu key (or Shift+F10) on a folio produced a bare Undo/Redo/Cut/Copy/Paste/Delete/Select All menu with almost everything disabled, instead of the menu a right-click gives. A keyboard-raised QContextMenuEvent carries no useful position -- Qt does not aim it at the selection. contextMenuEvent() passed the event to QGraphicsView first, which handed it to whichever item held focus; that item answered with its own default menu and accepted the event, so the early return fired and the folio's menu was never built. Even past that, the itemAt() lookup below would have used an unrelated point. A keyboard-raised menu is now built directly rather than offered to the items first, and aimed at the centre of the selection, or at the middle of the view when nothing is selected. The mouse path is unchanged. Measured on the same branch with only this change applied: before, the menu carried 7 actions, all but one disabled; after, 16, positioned on the selected element. Builds clean on Qt 5 and Qt 6, tests 5/5 on both. Co-Authored-By: Claude Opus 5 --- sources/diagramview.cpp | 70 ++++++++++++++++++++++++++++++++--------- 1 file changed, 55 insertions(+), 15 deletions(-) diff --git a/sources/diagramview.cpp b/sources/diagramview.cpp index 4b2d97563..825cabbdc 100644 --- a/sources/diagramview.cpp +++ b/sources/diagramview.cpp @@ -1218,30 +1218,70 @@ QList DiagramView::contextMenuActions() const */ void DiagramView::contextMenuEvent(QContextMenuEvent *e) { - QGraphicsView::contextMenuEvent(e); - if(e->isAccepted()) - return; + QPoint menu_pos = e->pos(); + QPoint menu_global_pos = e->globalPos(); + //A context menu raised from the keyboard (the Menu key, or + //Shift+F10) carries no useful position: Qt does not aim it at the + //selection. Two things then went wrong. QGraphicsView handed the + //event to whichever item held focus, which answered with its own + //generic Undo/Cut/Copy menu and accepted it, so the folio's real + //menu was never built; and had it got past that, itemAt() below + //would have looked up an unrelated point. + // + //So a keyboard-raised menu is built here directly rather than being + //offered to the items first, and aimed at the selection when there + //is one. The keyboard then gets the folio's menu, which is what a + //right-click gets. + const bool from_keyboard = e->reason() == QContextMenuEvent::Keyboard; - if (auto qgi = m_diagram->itemAt(mapToScene(e->pos()), transform())) + if (from_keyboard) { - if (!qgi->isSelected()) { - m_diagram->clearSelection(); + //Aim at the selection when there is one, so the menu appears + //beside what it acts on. With nothing selected there is nothing + //to aim at, so use the middle of the view -- the folio's own + //menu is still the right menu to show. + const auto selection = m_diagram->selectedItems(); + if (!selection.isEmpty()) + { + QRectF selection_rect; + for (auto *item : selection) { + selection_rect |= item->sceneBoundingRect(); + } + menu_pos = mapFromScene(selection_rect.center()); } + else + { + menu_pos = viewport()->rect().center(); + } + menu_global_pos = viewport()->mapToGlobal(menu_pos); + } + else + { + QGraphicsView::contextMenuEvent(e); + if(e->isAccepted()) + return; - // At this step qgi can be deleted for example if qgi is a QetGraphicsHandlerItem. - // When we call clearSelection the parent item of the handler - // is deselected and so delete all handlers, in this case, - // qgi become a dangling pointer. - // we need to call again itemAt. - if (auto item_ = m_diagram->itemAt(mapToScene(e->pos()), transform())) { - item_->setSelected(true); + if (auto qgi = m_diagram->itemAt(mapToScene(menu_pos), transform())) + { + if (!qgi->isSelected()) { + m_diagram->clearSelection(); + } + + // At this step qgi can be deleted for example if qgi is a QetGraphicsHandlerItem. + // When we call clearSelection the parent item of the handler + // is deselected and so delete all handlers, in this case, + // qgi become a dangling pointer. + // we need to call again itemAt. + if (auto item_ = m_diagram->itemAt(mapToScene(menu_pos), transform())) { + item_->setSelected(true); + } } } if (m_diagram->selectedItems().isEmpty()) { - m_paste_here_pos = e->pos(); + m_paste_here_pos = menu_pos; m_paste_here->setEnabled(Diagram::clipboardMayContainDiagram()); } @@ -1250,7 +1290,7 @@ void DiagramView::contextMenuEvent(QContextMenuEvent *e) { QMenu *context_menu = new QMenu(this); context_menu->addActions(list); - context_menu->popup(e->globalPos()); + context_menu->popup(menu_global_pos); e->accept(); } } From bd6bed8d619aadab582b1d2796e77ba7d871caac Mon Sep 17 00:00:00 2001 From: ispyisail Date: Tue, 15 Sep 2026 13:44:59 +1200 Subject: [PATCH 16/18] Open the menu bar on F10, and add a test that can answer whether it works MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit F10 opens the menu bar in most applications and is the usual way to reach the menus without a mouse. Qt provides this on Windows but not on X11, so on Linux the key did nothing and the press fell through to whichever widget had focus. It matters more here than it might elsewhere: "&Édition" takes É for its own letter, which is not on a UK or US keyboard, so that menu has no direct Alt route at all. A window-context QShortcut rather than a key handler -- key presses go to the focused child widget, so a keyPressEvent() on the window would never see F10 while the canvas or a panel has focus. tests/qttest/tst_menubarkeyboard.cpp covers three things: that Alt and a letter opens a menu (the control), that plain F10 does nothing in Qt itself (which is why the shortcut exists, and which will fail loudly if a future Qt starts handling it), and that the shortcut mechanism opens the bar. It uses QTest instead of driving a real X server for a specific reason. xdotool on Xvfb delivers every function key with Alt held: a Qt key logger shows Key_F10 arriving with modifiers == Qt::AltModifier. --clearmodifiers, keydown/keyup pairs, --window targeting and flattening the keycode with xmodmap all made no difference. Two rounds of GUI automation therefore gave confident, wrong answers about F10 -- first that it was broken, then that this very fix did not work. QTest posts the event straight to the widget, so the key arrives as written. What the test does not cover, since initCommonActions() calls QETApp::instance() and constructing that pulls in the whole application: it repeats the wiring rather than driving QETMainWindow. Confirming the real window responds still needs someone to press F10 in a running QElectroTech. Verified by breaking it: bound to F11 instead, the test fails. Qt 5 and Qt 6 both build clean, 6/6 tests on each. Co-Authored-By: Claude Opus 5 --- sources/qetmainwindow.cpp | 31 ++++++ sources/qetmainwindow.h | 1 + tests/qttest/CMakeLists.txt | 7 ++ tests/qttest/tst_menubarkeyboard.cpp | 138 +++++++++++++++++++++++++++ 4 files changed, 177 insertions(+) create mode 100644 tests/qttest/tst_menubarkeyboard.cpp diff --git a/sources/qetmainwindow.cpp b/sources/qetmainwindow.cpp index 29a9ce619..8447ffc58 100644 --- a/sources/qetmainwindow.cpp +++ b/sources/qetmainwindow.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include @@ -41,6 +42,14 @@ QETMainWindow::QETMainWindow(QWidget *widget, Qt::WindowFlags flags) : initCommonMenus(); setAcceptDrops(true); + //A shortcut rather than a key handler: a key press goes to the + //focused child widget, so a keyPressEvent() here would never see F10 + //while the canvas or a panel holds focus. + QShortcut *menu_bar_shortcut = new QShortcut(QKeySequence(Qt::Key_F10), this); + menu_bar_shortcut -> setContext(Qt::WindowShortcut); + connect(menu_bar_shortcut, &QShortcut::activated, + this, &QETMainWindow::activateMenuBar); + } /** @@ -251,6 +260,28 @@ void QETMainWindow::checkToolbarsmenu() /** Handle the \a e event. */ +/** + @brief QETMainWindow::activateMenuBar + Open the first usable menu, as pressing Alt and a menu's letter would. + + Qt implements F10 for this on Windows but not on X11, so on Linux the key + did nothing and the press fell through to whichever widget had focus. F10 + is the usual way to reach the menus without a mouse, and it matters here + because one menu cannot be reached by its own letter at all: "&Édition" + takes É, which is not on a UK or US keyboard. +*/ +void QETMainWindow::activateMenuBar() { + QMenuBar *bar = menuBar(); + if (!bar) return; + + for (QAction *action : bar -> actions()) { + if (action -> isVisible() && action -> isEnabled() && action -> menu()) { + bar -> setActiveAction(action); + return; + } + } +} + bool QETMainWindow::event(QEvent *e) { if (e -> type() == QEvent::WindowStateChange) { updateFullScreenAction(); diff --git a/sources/qetmainwindow.h b/sources/qetmainwindow.h index 8af17deef..3d90cee4a 100644 --- a/sources/qetmainwindow.h +++ b/sources/qetmainwindow.h @@ -39,6 +39,7 @@ class QETMainWindow : public QMainWindow { QAction *actionForMenu(QMenu *); protected: + void activateMenuBar(); bool event(QEvent *) override; void dragEnterEvent(QDragEnterEvent *e) override; void dropEvent(QDropEvent *e) override; diff --git a/tests/qttest/CMakeLists.txt b/tests/qttest/CMakeLists.txt index 11bbde5c7..e891d5aac 100644 --- a/tests/qttest/CMakeLists.txt +++ b/tests/qttest/CMakeLists.txt @@ -113,3 +113,10 @@ add_executable( add_test(NAME tst_qetstrings COMMAND tst_qetstrings) target_include_directories(tst_qetstrings PRIVATE ${QET_DIR}/sources) target_link_libraries(tst_qetstrings PRIVATE Qt::Test Qt::Widgets Qt::Xml) + +add_executable( + tst_menubarkeyboard + tst_menubarkeyboard.cpp) +add_test(NAME tst_menubarkeyboard COMMAND tst_menubarkeyboard) +target_include_directories(tst_menubarkeyboard PRIVATE ${QET_DIR}/sources) +target_link_libraries(tst_menubarkeyboard PRIVATE Qt::Test Qt::Widgets) diff --git a/tests/qttest/tst_menubarkeyboard.cpp b/tests/qttest/tst_menubarkeyboard.cpp new file mode 100644 index 000000000..79821a2b5 --- /dev/null +++ b/tests/qttest/tst_menubarkeyboard.cpp @@ -0,0 +1,138 @@ +/* + Copyright 2006-2026 The QElectroTech Team + This file is part of QElectroTech. + + QElectroTech is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + QElectroTech is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QElectroTech. If not, see . +*/ +#include +#include +#include +#include +#include + +/* + F10 should open the menu bar, as it does in most applications and as + someone working without a mouse will expect. Qt provides this on Windows + but not on X11, so QElectroTech adds it (QETMainWindow::activateMenuBar). + + These tests use QTest rather than driving a real X server. That is not a + convenience: xdotool on Xvfb delivers every function key with Alt held, so + the application receives Alt+F10 and never the plain key. Two rounds of + GUI automation gave confident, wrong answers about F10 before that was + understood. QTest posts the event directly to the widget, so the key + arrives exactly as written. +*/ +class TstMenuBarKeyboard : public QObject +{ + Q_OBJECT + + private slots: + void altLetterOpensMenu(); // control -- must pass, or nothing below means anything + void plainF10DoesNothingInQt(); // the gap being filled + void shortcutOpensMenuBar(); // the mechanism QETMainWindow uses +}; + +namespace { + +QMainWindow *makeWindow(QMenu **file_menu) +{ + auto *w = new QMainWindow; + *file_menu = w->menuBar()->addMenu(QStringLiteral("&File")); + (*file_menu)->addAction(QStringLiteral("Quit")); + w->menuBar()->addMenu(QStringLiteral("&Edit"))->addAction(QStringLiteral("Copy")); + w->resize(600, 400); + w->show(); + w->activateWindow(); + w->raise(); + return w; +} + +} + +/* + The control. Alt and a menu's letter is known to work, so if this fails + the environment cannot open menus at all and the other two tests are + measuring nothing. +*/ +void TstMenuBarKeyboard::altLetterOpensMenu() +{ + QMenu *file = nullptr; + QScopedPointer w(makeWindow(&file)); + QVERIFY(QTest::qWaitForWindowExposed(w.data())); + + QTest::keyClick(w.data(), Qt::Key_F, Qt::AltModifier); + QTest::qWait(300); + + QVERIFY2(file->isVisible(), + "control failed: Alt+F did not open a menu, so this environment " + "cannot judge any of the keyboard tests below"); +} + +/* + Records why the shortcut in QETMainWindow exists. If a future Qt starts + handling F10 on this platform, this test fails and the shortcut can go. +*/ +void TstMenuBarKeyboard::plainF10DoesNothingInQt() +{ + QMenu *file = nullptr; + QScopedPointer w(makeWindow(&file)); + QVERIFY(QTest::qWaitForWindowExposed(w.data())); + + QTest::keyClick(w.data(), Qt::Key_F10, Qt::NoModifier); + QTest::qWait(300); + + QVERIFY2(!file->isVisible() && w->menuBar()->activeAction() == nullptr, + "Qt now handles F10 by itself -- QETMainWindow's shortcut is " + "redundant and can be removed"); +} + +/* + The mechanism QETMainWindow uses, exercised on a plain QMainWindow. + + Worth being clear about what this does not cover: it repeats the shortcut + wiring rather than driving QETMainWindow itself, because + initCommonActions() calls QETApp::instance() and constructing that pulls + in the whole application -- element collections and all -- which does not + belong in a unit test. So this proves the approach works and would catch + it breaking in a future Qt; it does not prove QETMainWindow is wired up. + That last step needs someone to press F10 in a running QElectroTech. +*/ +void TstMenuBarKeyboard::shortcutOpensMenuBar() +{ + QMenu *file = nullptr; + QScopedPointer holder(makeWindow(&file)); + QMainWindow *w = holder.data(); + QVERIFY(QTest::qWaitForWindowExposed(w)); + + auto *shortcut = new QShortcut(QKeySequence(Qt::Key_F10), w); + shortcut->setContext(Qt::WindowShortcut); + QObject::connect(shortcut, &QShortcut::activated, w, [w]() { + for (QAction *action : w->menuBar()->actions()) { + if (action->isVisible() && action->isEnabled() && action->menu()) { + w->menuBar()->setActiveAction(action); + return; + } + } + }); + + QTest::keyClick(w, Qt::Key_F10, Qt::NoModifier); + QTest::qWait(300); + + QVERIFY2(w->menuBar()->activeAction() != nullptr, + "F10 did not activate the menu bar"); + QCOMPARE(w->menuBar()->activeAction()->text(), QStringLiteral("&File")); +} + +QTEST_MAIN(TstMenuBarKeyboard) +#include "tst_menubarkeyboard.moc" From b94b24491951446d151e06d12dc3b9a3f87fae3d Mon Sep 17 00:00:00 2001 From: ispyisail Date: Tue, 15 Sep 2026 15:51:24 +1200 Subject: [PATCH 17/18] Correct the comment: say what was measured, not what was assumed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two claims in the previous comment were wrong. "Qt implements F10 on Windows but not on X11" was inference I cannot test here. What is measured is narrower and enough: QMenuBar given Key_F10 directly leaves it unaccepted, and sent to the window the key never reaches the menu bar at all, because a key press goes to the focused child widget. "&Édition takes É, which is not on a UK or US keyboard" was wrong outright. It came from running an uninstalled binary, which cannot find its .qm files and falls back to the French source strings. With translations loaded the menus read File, Edit, Project, Display, Settings, Windows, Help, and Alt+E opens Edit. The comment now also says plainly that this is convenience rather than access: Alt tap focuses the bar and Alt with a letter opens a menu, both verified working, so the menus were already reachable without a mouse. F10 is the key people reach for out of habit. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GtMZqGEiUMvDBqcFvVG2vb --- sources/qetmainwindow.cpp | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/sources/qetmainwindow.cpp b/sources/qetmainwindow.cpp index 8447ffc58..efc9f0e83 100644 --- a/sources/qetmainwindow.cpp +++ b/sources/qetmainwindow.cpp @@ -264,11 +264,18 @@ void QETMainWindow::checkToolbarsmenu() @brief QETMainWindow::activateMenuBar Open the first usable menu, as pressing Alt and a menu's letter would. - Qt implements F10 for this on Windows but not on X11, so on Linux the key - did nothing and the press fell through to whichever widget had focus. F10 - is the usual way to reach the menus without a mouse, and it matters here - because one menu cannot be reached by its own letter at all: "&Édition" - takes É, which is not on a UK or US keyboard. + F10 is what most applications use for this, and QMenuBar does not handle + it: given the key directly it leaves it unaccepted, and sent to the window + it never reaches the menu bar at all, because a key press goes to the + focused child widget. So the press fell through to whichever widget had + focus and looked like nothing happening. + + This is convenience, not access. Qt already provides two keyboard routes + into the menus and both work: a bare Alt tap focuses the bar, and Alt with + a menu's letter opens it. This adds the key people reach for out of habit. + + A shortcut rather than a keyPressEvent() override, for the reason above -- + the window never sees the key while a child holds focus. */ void QETMainWindow::activateMenuBar() { QMenuBar *bar = menuBar(); From 55c2c0df9d2c35e614bfca31f800eaf35a50e44c Mon Sep 17 00:00:00 2001 From: ispyisail Date: Tue, 15 Sep 2026 17:07:15 +1200 Subject: [PATCH 18/18] Paste under the cursor and let it be positioned before it lands Ctrl+V pasted in place, which put the copy exactly on top of the original. Nothing appeared to happen: the only clue was a doubled outline, and the copy had to be dragged off the original to be seen at all. The cursor was ignored entirely. Ctrl+V now starts a placement. The items appear under the cursor and follow it until a left click or Return drops them; Escape or a right click takes them away again. That is the same interaction as placing a new element, so paste behaves like every other way of putting something on a folio, and the copy lands where the user is looking. Implemented as a DiagramEventInterface beside the existing add-element and add-macro tools. The pasted items are the real ones from the start rather than a preview: Diagram::fromXml creates them exactly as before, this class moves them, and PasteDiagramCommand is pushed only once they are dropped. PasteDiagramCommand's first redo() deliberately does not add items to the scene -- it assumes fromXml already did -- so pushing it on commit adopts them rather than duplicating them. One copy of the paste logic, and a cancelled paste leaves nothing on the undo stack. Conductors are not moved directly; they are drawn from their terminals and follow the elements they attach to. On cancel they are removed before the elements, so none is left in the scene holding a pointer to a freed terminal. Verified by counting elements in the saved file rather than by eye: 56 to start, 56 after paste-then-Escape, 57 after paste-then-drop, and 56 again after undo. Save determinism run against this build: pass, no regressions against baseline. Tests 5/5 on Qt 6. Co-Authored-By: Claude Opus 5 --- cmake/qet_compilation_vars.cmake | 2 + sources/diagramevent/diagrameventaddpaste.cpp | 241 ++++++++++++++++++ sources/diagramevent/diagrameventaddpaste.h | 85 ++++++ sources/qetdiagrameditor.cpp | 18 +- 4 files changed, 344 insertions(+), 2 deletions(-) create mode 100644 sources/diagramevent/diagrameventaddpaste.cpp create mode 100644 sources/diagramevent/diagrameventaddpaste.h diff --git a/cmake/qet_compilation_vars.cmake b/cmake/qet_compilation_vars.cmake index 3fdb11e08..a8654b903 100644 --- a/cmake/qet_compilation_vars.cmake +++ b/cmake/qet_compilation_vars.cmake @@ -330,6 +330,8 @@ set(QET_SRC_FILES ${QET_DIR}/sources/diagramevent/diagrameventinterface.h ${QET_DIR}/sources/diagramevent/diagrameventaddmacro.cpp ${QET_DIR}/sources/diagramevent/diagrameventaddmacro.h + ${QET_DIR}/sources/diagramevent/diagrameventaddpaste.cpp + ${QET_DIR}/sources/diagramevent/diagrameventaddpaste.h ${QET_DIR}/sources/dvevent/dveventinterface.cpp ${QET_DIR}/sources/dvevent/dveventinterface.h diff --git a/sources/diagramevent/diagrameventaddpaste.cpp b/sources/diagramevent/diagrameventaddpaste.cpp new file mode 100644 index 000000000..b08406b68 --- /dev/null +++ b/sources/diagramevent/diagrameventaddpaste.cpp @@ -0,0 +1,241 @@ +/* + Copyright 2006-2026 The QElectroTech Team + This file is part of QElectroTech. + + QElectroTech is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + QElectroTech is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QElectroTech. If not, see . +*/ +#include "diagrameventaddpaste.h" + +#include "../diagram.h" +#include "../diagramcommands.h" +#include "../qetapp.h" +#include "../qetdiagrameditor.h" +#include "../qetgraphicsitem/conductor.h" + +#include +#include +#include +#include +#include + +/** + @brief DiagramEventAddPaste::DiagramEventAddPaste + @param diagram : diagram to paste into + @param start_pos : where the pasted items first appear, in scene + coordinates -- normally the cursor +*/ +DiagramEventAddPaste::DiagramEventAddPaste(Diagram *diagram, const QPointF &start_pos) : + DiagramEventInterface(diagram) +{ + //DiagramEventInterface::init() is called by Diagram::setEventInterface + //only when it is replacing an earlier interface, so call it here as + //DiagramEventAddMacro does. + init(); + + const QString clipboard_text = QApplication::clipboard()->text(); + if (clipboard_text.isEmpty()) return; + + QDomDocument document_xml; + if (!document_xml.setContent(clipboard_text)) return; + + m_diagram->fromXml(document_xml, Diagram::snapToGrid(start_pos), false, &m_content); + if (!m_content.count()) return; + + //Remember where each item sits relative to the group's top left, so a + //move is one assignment per item rather than an accumulated delta. + QRectF group_rect; + const QList movable = m_content.items(MovableItems); + for (auto *item : movable) { + group_rect = group_rect.united(item->mapToScene(item->boundingRect()).boundingRect()); + } + const QPointF top_left = group_rect.topLeft(); + for (auto *item : movable) { + m_relative_pos.insert(item, item->pos() - top_left); + } + + m_diagram->clearSelection(); + for (auto *item : movable) { + item->setSelected(true); + } + + if (!m_diagram->views().isEmpty()) { + if (const auto qde = QETApp::diagramEditorAncestorOf(m_diagram->views().at(0))) { + m_status_bar = qde->statusBar(); + } + } + showHint(); + + m_running = true; +} + +/** + @brief DiagramEventAddPaste::~DiagramEventAddPaste + If the placement never finished -- the editor closed, or another tool took + over -- the items are still on the folio with nothing on the undo stack to + account for them, so take them away. +*/ +DiagramEventAddPaste::~DiagramEventAddPaste() +{ + if (!m_finished && m_diagram) { + cancel(); + } + if (m_status_bar) { + m_status_bar->clearMessage(); + } +} + +/** + @brief DiagramEventAddPaste::clipboardHasDiagram + @return true if the clipboard holds a diagram fragment +*/ +bool DiagramEventAddPaste::clipboardHasDiagram() +{ + return Diagram::clipboardMayContainDiagram(); +} + +/** + @brief DiagramEventAddPaste::init + Suppress the context menu while placing, so a right click can cancel + instead of opening a menu over the items being positioned. +*/ +void DiagramEventAddPaste::init() +{ + if (!m_diagram) return; + const auto views = m_diagram->views(); + for (auto *view : views) { + view->setContextMenuPolicy(Qt::NoContextMenu); + } +} + +void DiagramEventAddPaste::showHint() +{ + if (m_status_bar) { + m_status_bar->showMessage( + tr("Cliquez pour poser le collage, Échap ou clic droit pour annuler", + "status bar tip while positioning a paste")); + } +} + +/** + @brief DiagramEventAddPaste::moveTo + Put the group's top left corner at @a scene_pos, snapped to the grid. +*/ +void DiagramEventAddPaste::moveTo(const QPointF &scene_pos) +{ + const QPointF anchor = Diagram::snapToGrid(scene_pos); + for (auto it = m_relative_pos.constBegin() ; it != m_relative_pos.constEnd() ; ++it) { + if (it.key()) { + it.key()->setPos(anchor + it.value()); + } + } +} + +void DiagramEventAddPaste::mouseMoveEvent(QGraphicsSceneMouseEvent *event) +{ + if (!m_running) return; + moveTo(event->scenePos()); + event->setAccepted(true); +} + +void DiagramEventAddPaste::mousePressEvent(QGraphicsSceneMouseEvent *event) +{ + if (!m_running) return; + //Swallowed so the press cannot start a rubber band or drag an item + //out of the group; the release is what decides. + event->setAccepted(true); +} + +void DiagramEventAddPaste::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) +{ + if (!m_running) return; + + if (event->button() == Qt::LeftButton) { + moveTo(event->scenePos()); + commit(); + } else if (event->button() == Qt::RightButton) { + cancel(); + } + event->setAccepted(true); +} + +void DiagramEventAddPaste::keyPressEvent(QKeyEvent *event) +{ + if (!m_running) return; + + switch (event->key()) { + case Qt::Key_Escape: + cancel(); + event->setAccepted(true); + break; + //Return and Enter drop the paste where it stands, so the whole + //operation can be completed without a mouse. + case Qt::Key_Return: + case Qt::Key_Enter: + commit(); + event->setAccepted(true); + break; + default: + break; + } +} + +/** + @brief DiagramEventAddPaste::commit + Hand the items to the undo stack where they stand. + + PasteDiagramCommand's first redo() does not add the items to the scene -- + it assumes they are already there, which is what Diagram::fromXml did when + this started. So pushing it here adopts them rather than duplicating them. +*/ +void DiagramEventAddPaste::commit() +{ + if (m_finished || !m_diagram) return; + m_finished = true; + m_running = false; + + m_diagram->undoStack().push(new PasteDiagramCommand(m_diagram, m_content)); + emit finish(); +} + +/** + @brief DiagramEventAddPaste::cancel + Take the items back off the folio. Nothing was pushed to the undo stack, + so there is nothing to undo afterwards. +*/ +void DiagramEventAddPaste::cancel() +{ + if (m_finished || !m_diagram) return; + m_finished = true; + m_running = false; + + //Conductors first: they hold pointers to the terminals of the + //elements below, so removing an element out from under one would + //leave it pointing at freed memory for as long as it is still in the + //scene. + const QList conductors = m_content.conductors(DiagramContent::AnyConductor); + for (auto *conductor : conductors) { + m_diagram->removeItem(conductor); + delete conductor; + } + + const QList rest = m_content.items(MovableItems); + for (auto *item : rest) { + m_diagram->removeItem(item); + delete item; + } + + m_content.clear(); + m_relative_pos.clear(); + emit finish(); +} diff --git a/sources/diagramevent/diagrameventaddpaste.h b/sources/diagramevent/diagrameventaddpaste.h new file mode 100644 index 000000000..a200ca04d --- /dev/null +++ b/sources/diagramevent/diagrameventaddpaste.h @@ -0,0 +1,85 @@ +/* + Copyright 2006-2026 The QElectroTech Team + This file is part of QElectroTech. + + QElectroTech is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + QElectroTech is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QElectroTech. If not, see . +*/ +#ifndef DIAGRAMEVENTADDPASTE_H +#define DIAGRAMEVENTADDPASTE_H + +#include "diagrameventinterface.h" +#include "../diagramcontent.h" + +#include +#include + +class QStatusBar; + +/** + @brief The DiagramEventAddPaste class + Paste the clipboard as a placement you can still move. + + The pasted items are added straight away and follow the cursor until a + left click drops them; Escape or a right click takes them away again. + This is the same interaction as placing a new element, so a paste behaves + like every other "put something on the folio" action. + + The items are the real ones from the start, not a preview: Diagram::fromXml + creates them, this class moves them, and PasteDiagramCommand is pushed only + once they are dropped. That keeps one copy of the paste logic rather than + two, and means a cancelled paste leaves nothing on the undo stack. +*/ +class DiagramEventAddPaste : public DiagramEventInterface +{ + Q_OBJECT + + public: + ///Items with a position of their own. Conductors are left out + ///deliberately: they are drawn from their terminals, so they + ///follow when the elements they attach to move. + static const int MovableItems = + DiagramContent::Elements + | DiagramContent::TextFields + | DiagramContent::Images + | DiagramContent::Shapes + | DiagramContent::Tables + | DiagramContent::TerminalStrip; + + DiagramEventAddPaste(Diagram *diagram, const QPointF &start_pos); + ~DiagramEventAddPaste() override; + + void mouseMoveEvent (QGraphicsSceneMouseEvent *event) override; + void mousePressEvent (QGraphicsSceneMouseEvent *event) override; + void mouseReleaseEvent (QGraphicsSceneMouseEvent *event) override; + void keyPressEvent (QKeyEvent *event) override; + void init() override; + + ///@return true if the clipboard holds something this can paste. + static bool clipboardHasDiagram(); + + private: + void moveTo(const QPointF &scene_pos); + void commit(); + void cancel(); + void showHint(); + + DiagramContent m_content; + ///Each movable item's position relative to the group's top left, + ///taken once so repeated moves cannot accumulate rounding drift. + QHash m_relative_pos; + QPointer m_status_bar; + bool m_finished{false}; +}; + +#endif // DIAGRAMEVENTADDPASTE_H diff --git a/sources/qetdiagrameditor.cpp b/sources/qetdiagrameditor.cpp index cf6f7d8fd..c771e38c8 100644 --- a/sources/qetdiagrameditor.cpp +++ b/sources/qetdiagrameditor.cpp @@ -29,6 +29,7 @@ #include "diagramevent/diagrameventaddshape.h" #include "diagramevent/diagrameventaddpath.h" #include "diagramevent/diagrameventaddtext.h" +#include "diagramevent/diagrameventaddpaste.h" #include "diagramview.h" #include "elementspanelwidget.h" #include "factory/qetgraphicstablefactory.h" @@ -349,8 +350,21 @@ void QETDiagramEditor::setUpActions() currentDiagramView()->copy(); }); connect(m_paste, &QAction::triggered, [this]() { - if(currentDiagramView()) - currentDiagramView()->paste(); + auto *dv = currentDiagramView(); + if (!dv || !dv->diagram()) return; + + //Paste as a placement rather than dropping the items straight + //down. Pasting in place put the copy exactly on top of the + //original, where it was easy to miss entirely; now it appears + //under the cursor and follows it until a click, Return, or Escape + //to cancel -- the same interaction as placing a new element. + const QPoint view_pos = dv->viewport()->mapFromGlobal(QCursor::pos()); + const QPointF start_pos = dv->viewport()->rect().contains(view_pos) + ? dv->mapToScene(view_pos) + : dv->mapToScene(dv->viewport()->rect().center()); + + dv->diagram()->setEventInterface( + new DiagramEventAddPaste(dv->diagram(), start_pos)); }); //Reset conductor path