From 7ba820f8f0b6d7fec3b29776a915876c78e6b5b3 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Sat, 3 Oct 2026 23:16:43 +1300 Subject: [PATCH] Set the wires-per-terminal limits for the application, a project can override MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The limits were per project only. They are now set once in Settings > General, under the master switch: the most wires per terminal and "one wire per folio report". Every project follows them. Project properties > "Conducteurs par borne" gets "Utiliser les réglages de l'application", ticked by default, showing the application's values greyed. Unticked, the project keeps its own values, saved in the project as before, so a drawing that needs its own limit carries it to any computer. A project's own "no limit" overrides an application limit too. QETProject::wiringRules() now returns the rules in force (the project's own, or the application's), so the code that checks a wire is unchanged; projectWiringRules() returns what the project itself stores. A project that follows the application still writes nothing, so it saves exactly as before. tst_wiringrules: which rules win, the save round trip of a project's own "no rule", and through the real binary that the application's limit refuses a wire unless the project overrides it or the master switch is off (checked to fail with the application's rules ignored). ctest 55/55. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015FPuYPS4T7QuEwjNu22rXD --- sources/qetproject.cpp | 17 ++++- sources/qetproject.h | 1 + .../configpage/generalconfigurationpage.cpp | 19 +++++ .../ui/configpage/generalconfigurationpage.ui | 53 ++++++++++++++ sources/ui/configpage/projectconfigpages.cpp | 50 +++++++++++-- sources/ui/configpage/projectconfigpages.h | 2 + sources/wiringrules.cpp | 59 +++++++++++++-- sources/wiringrules.h | 21 +++++- tests/qttest/tst_wiringrules.cpp | 72 ++++++++++++++++--- 9 files changed, 268 insertions(+), 26 deletions(-) diff --git a/sources/qetproject.cpp b/sources/qetproject.cpp index 4a20fd926..e8ce5998d 100644 --- a/sources/qetproject.cpp +++ b/sources/qetproject.cpp @@ -1260,16 +1260,27 @@ void QETProject::setWireHops(WireHops::Mode mode) /** @brief QETProject::wiringRules @return how many wires a terminal of this project may take - (discussion #1158); the default when the project sets no rule. + (discussion #1158): the project's own rules when it sets them, + otherwise the application's (Settings > General). */ WiringRules::Settings QETProject::wiringRules() const { + return WiringRules::effective(m_wiring_rules, WiringRules::applicationSettings()); +} + +/** + @brief QETProject::projectWiringRules + @return the rules as the project stores them: Settings::own false when + it follows the application's. +*/ +WiringRules::Settings QETProject::projectWiringRules() const { return m_wiring_rules; } /** @brief QETProject::setWiringRules - Set how many wires a terminal of this project may take. Only wires - drawn from now on are affected: none already drawn is removed. + Set the project's own wiring rules, or (Settings::own false) make it + follow the application's. Only wires drawn from now on are affected: + none already drawn is removed. @param rules */ void QETProject::setWiringRules(const WiringRules::Settings &rules) diff --git a/sources/qetproject.h b/sources/qetproject.h index 2f713919e..90fcbd0fb 100644 --- a/sources/qetproject.h +++ b/sources/qetproject.h @@ -213,6 +213,7 @@ class QETProject : public QObject WireHops::Mode wireHops() const; void setWireHops(WireHops::Mode mode); WiringRules::Settings wiringRules() const; + WiringRules::Settings projectWiringRules() const; void setWiringRules(const WiringRules::Settings &rules); void setAutoBreakConductor (bool abc); void setAutoElement (bool ae); diff --git a/sources/ui/configpage/generalconfigurationpage.cpp b/sources/ui/configpage/generalconfigurationpage.cpp index 7a6293b81..aa6e63989 100644 --- a/sources/ui/configpage/generalconfigurationpage.cpp +++ b/sources/ui/configpage/generalconfigurationpage.cpp @@ -115,6 +115,19 @@ GeneralConfigurationPage::GeneralConfigurationPage(QWidget *parent) : ui->m_zoom_out_beyond_folio->setChecked(settings.value("diagrameditor/zoom-out-beyond-of-folio", false).toBool()); ui->m_conductor_properties_panel->setChecked(settings.value("diagrameditor/conductor_properties_panel", false).toBool()); ui->m_wiring_rules_cb->setChecked(WiringRules::masterEnabled()); + { + //The rules every project follows unless it sets its own (#1158) + const WiringRules::Settings rules = WiringRules::applicationSettings(); + ui->m_wiring_max_wires_sb->setValue(rules.max_wires); + ui->m_wiring_one_wire_per_report_cb->setChecked(rules.one_wire_per_report); + auto enable = [this](bool on) { + ui->m_wiring_max_wires_label->setEnabled(on); + ui->m_wiring_max_wires_sb->setEnabled(on); + ui->m_wiring_one_wire_per_report_cb->setEnabled(on); + }; + enable(ui->m_wiring_rules_cb->isChecked()); + connect(ui->m_wiring_rules_cb, &QCheckBox::toggled, this, enable); + } ui->m_use_gesture_trackpad->setChecked(settings.value("diagramview/gestures", false).toBool()); ui->m_save_label_paste->setChecked(settings.value("diagramcommands/erase-label-on-copy", true).toBool()); ui->m_enable_scripting->setChecked(QetSettings::scriptingEnabled()); @@ -337,6 +350,12 @@ void GeneralConfigurationPage::applyConf() settings.setValue("diagrameditor/zoom-out-beyond-of-folio", ui->m_zoom_out_beyond_folio->isChecked()); settings.setValue("diagrameditor/conductor_properties_panel", ui->m_conductor_properties_panel->isChecked()); WiringRules::setMasterEnabled(ui->m_wiring_rules_cb->isChecked()); + { + WiringRules::Settings rules = WiringRules::applicationSettings(); + rules.max_wires = ui->m_wiring_max_wires_sb->value(); + rules.one_wire_per_report = ui->m_wiring_one_wire_per_report_cb->isChecked(); + WiringRules::setApplicationSettings(rules); + } settings.setValue("diagrameditor/autosave-interval", ui->m_autosave_sb->value()); settings.setValue("diagrameditor/grid_display_startup", ui->grid_startup_cb->isChecked()); diff --git a/sources/ui/configpage/generalconfigurationpage.ui b/sources/ui/configpage/generalconfigurationpage.ui index adf7e3a21..1120faa20 100644 --- a/sources/ui/configpage/generalconfigurationpage.ui +++ b/sources/ui/configpage/generalconfigurationpage.ui @@ -83,6 +83,57 @@ + + + + 20 + + + + + Nombre maximal de conducteurs par borne : + + + + + + + Pour tous les projets, sauf ceux qui ont leurs propres réglages. Un nouveau conducteur qui dépasserait ce nombre sur une borne est refusé ; les conducteurs déjà dessinés ne sont pas modifiés. 4 correspond à deux embouts doubles, un de chaque côté de la vis. + + + Sans limite + + + 99 + + + + + + + Qt::Horizontal + + + + + + + + + 20 + + + + + Un renvoi de folio est un point virtuel : il ne reçoit qu'un conducteur, celui qui continue sur l'autre folio. + + + Un seul conducteur par renvoi de folio + + + + + @@ -1311,6 +1362,8 @@ Vous pouvez spécifier ici la valeur par défaut de ce champ pour les éléments m_zoom_out_beyond_folio m_conductor_properties_panel m_wiring_rules_cb + m_wiring_max_wires_sb + m_wiring_one_wire_per_report_cb m_use_windows_mode_rb m_use_tab_mode_rb m_save_label_paste diff --git a/sources/ui/configpage/projectconfigpages.cpp b/sources/ui/configpage/projectconfigpages.cpp index d1ac9520d..4531ffcea 100644 --- a/sources/ui/configpage/projectconfigpages.cpp +++ b/sources/ui/configpage/projectconfigpages.cpp @@ -177,10 +177,14 @@ void ProjectMainConfigPage::applyProjectConf() modified_project = true; } - WiringRules::Settings wiring_rules = m_project -> wiringRules(); - wiring_rules.max_wires = max_wires_sb_ -> value(); - wiring_rules.one_wire_per_report = one_wire_per_report_cb_ -> isChecked(); - if (m_project -> wiringRules() != wiring_rules) { + //Following the application's rules stores nothing in the project + WiringRules::Settings wiring_rules; + if (!use_application_rules_cb_ -> isChecked()) { + wiring_rules.own = true; + wiring_rules.max_wires = max_wires_sb_ -> value(); + wiring_rules.one_wire_per_report = one_wire_per_report_cb_ -> isChecked(); + } + if (m_project -> projectWiringRules() != wiring_rules) { m_project -> setWiringRules(wiring_rules); modified_project = true; } @@ -237,6 +241,20 @@ void ProjectMainConfigPage::initWidgets() //How many wires a terminal may take (discussion #1158) wiring_rules_gb_ = new QGroupBox(tr("Conducteurs par borne", "group box title")); + use_application_rules_cb_ = new QCheckBox(tr("Utiliser les réglages de l'application", "checkbox label")); + use_application_rules_cb_ -> setToolTip(tr("Les réglages de Configurer QElectroTech > Général s'appliquent. " + "Décochez pour donner à ce projet ses propres réglages, enregistrés dans le projet.", + "tooltip")); + connect(use_application_rules_cb_, &QCheckBox::toggled, this, [this](bool use) { + //Show the values that will apply: the application's, or the + //application's as a starting point for the project's own + if (use) { + const WiringRules::Settings application = WiringRules::applicationSettings(); + max_wires_sb_ -> setValue(application.max_wires); + one_wire_per_report_cb_ -> setChecked(application.one_wire_per_report); + } + updateWiringRulesWidgets(); + }); max_wires_label_ = new QLabel(tr("Nombre maximal de conducteurs par borne :", "label when configuring")); max_wires_sb_ = new QSpinBox(); max_wires_sb_ -> setRange(0, 99); @@ -288,6 +306,7 @@ void ProjectMainConfigPage::initLayout() main_layout0 -> addSpacing(10); QVBoxLayout *wiring_rules_layout = new QVBoxLayout(wiring_rules_gb_); + wiring_rules_layout -> addWidget(use_application_rules_cb_); QHBoxLayout *max_wires_layout = new QHBoxLayout(); max_wires_layout -> addWidget(max_wires_label_); max_wires_layout -> addWidget(max_wires_sb_); @@ -320,16 +339,33 @@ void ProjectMainConfigPage::readValuesFromProject() const int wire_hops_index = wire_hops_cb_ -> findData(WireHops::toString(m_project -> wireHops())); wire_hops_cb_ -> setCurrentIndex(qMax(0, wire_hops_index)); + //The rules that apply: the project's own or the application's const WiringRules::Settings wiring_rules = m_project -> wiringRules(); + { + const QSignalBlocker blocker(use_application_rules_cb_); + use_application_rules_cb_ -> setChecked(!wiring_rules.own); + } max_wires_sb_ -> setValue(wiring_rules.max_wires); one_wire_per_report_cb_ -> setChecked(wiring_rules.one_wire_per_report); + updateWiringRulesWidgets(); +} + +/** + @brief ProjectMainConfigPage::updateWiringRulesWidgets + Enable the wiring rule fields only when they can change something: the + master switch is on and the project sets its own rules. +*/ +void ProjectMainConfigPage::updateWiringRulesWidgets() +{ //The master switch wins over every project: say so rather than //let the user set a rule that does nothing. const bool master = WiringRules::masterEnabled(); + const bool own = master && !use_application_rules_cb_ -> isChecked(); wiring_rules_off_label_ -> setVisible(!master); - max_wires_label_ -> setEnabled(master); - max_wires_sb_ -> setEnabled(master); - one_wire_per_report_cb_ -> setEnabled(master); + use_application_rules_cb_ -> setEnabled(master); + max_wires_label_ -> setEnabled(own); + max_wires_sb_ -> setEnabled(own); + one_wire_per_report_cb_ -> setEnabled(own); } /** diff --git a/sources/ui/configpage/projectconfigpages.h b/sources/ui/configpage/projectconfigpages.h index 0868815d1..e911b5a43 100644 --- a/sources/ui/configpage/projectconfigpages.h +++ b/sources/ui/configpage/projectconfigpages.h @@ -116,6 +116,7 @@ class ProjectMainConfigPage : public ProjectConfigPage { private slots: void resetUsageTracker(); + void updateWiringRulesWidgets(); // attributes protected: @@ -131,6 +132,7 @@ class ProjectMainConfigPage : public ProjectConfigPage { QLabel *wire_hops_label_; QComboBox *wire_hops_cb_; QGroupBox *wiring_rules_gb_; + QCheckBox *use_application_rules_cb_; QLabel *max_wires_label_; QSpinBox *max_wires_sb_; QCheckBox *one_wire_per_report_cb_; diff --git a/sources/wiringrules.cpp b/sources/wiringrules.cpp index fcc3a66a2..d0d4e5fc8 100644 --- a/sources/wiringrules.cpp +++ b/sources/wiringrules.cpp @@ -30,6 +30,9 @@ namespace { //The master switch is read on every wire drawn, so it is read from //the settings once and kept; setMasterEnabled() keeps both in step. int master_cache = -1; + //Same for the application's rules + bool application_cached = false; + WiringRules::Settings application_cache; } /** @@ -57,11 +60,56 @@ void WiringRules::setMasterEnabled(bool enabled) master_cache = enabled ? 1 : 0; } +/** + @brief WiringRules::applicationSettings + @return the rules set for every project in Settings > General; a + project that sets its own (Settings::own) uses those instead. +*/ +WiringRules::Settings WiringRules::applicationSettings() +{ + if (!application_cached) { + const QSettings settings; + application_cache = Settings(); + application_cache.max_wires = qMax(0, settings.value(max_wires_key, 0).toInt()); + application_cache.one_wire_per_report = settings.value(one_wire_per_report_key, false).toBool(); + application_cached = true; + } + return application_cache; +} + +/** + @brief WiringRules::setApplicationSettings + Save the rules set for every project. + @param settings +*/ +void WiringRules::setApplicationSettings(const Settings &settings) +{ + QSettings qsettings; + qsettings.setValue(max_wires_key, qMax(0, settings.max_wires)); + qsettings.setValue(one_wire_per_report_key, settings.one_wire_per_report); + application_cache = settings; + application_cache.own = false; + application_cached = true; +} + +/** + @brief WiringRules::effective + @return the rules that apply to a project: its own when it sets them, + otherwise the application's. +*/ +WiringRules::Settings WiringRules::effective(const Settings &project, const Settings &application) +{ + Settings result = project.own ? project : application; + result.own = project.own; + return result; +} + /** @brief WiringRules::fromXml @param project_root : the root element of a project @return the rules stored in the child of \a project_root, - the default (no rule) when there is none. + which the project then uses instead of the application's (Settings::own); + the default, which follows the application, when there is none. */ WiringRules::Settings WiringRules::fromXml(const QDomElement &project_root) { @@ -70,6 +118,7 @@ WiringRules::Settings WiringRules::fromXml(const QDomElement &project_root) if (rules.isNull()) { return settings; } + settings.own = true; settings.max_wires = qMax(0, rules.attribute(max_wires_attribute, QStringLiteral("0")).toInt()); settings.one_wire_per_report = rules.attribute(report_attribute) == QLatin1String("true"); return settings; @@ -77,13 +126,13 @@ WiringRules::Settings WiringRules::fromXml(const QDomElement &project_root) /** @brief WiringRules::toXml - Write \a settings as a child of \a project_root. Nothing - is written when no rule is on, so a project that never used them saves - exactly as before. + Write \a settings as a child of \a project_root, only + when the project sets its own rules: a project that follows the + application's saves exactly as before. */ void WiringRules::toXml(const Settings &settings, QDomElement &project_root) { - if (settings.isDefault()) { + if (!settings.own) { return; } QDomElement rules = project_root.ownerDocument().createElement(element_name); diff --git a/sources/wiringrules.h b/sources/wiringrules.h index f9cab9247..af6311886 100644 --- a/sources/wiringrules.h +++ b/sources/wiringrules.h @@ -29,7 +29,10 @@ class QDomElement; limit, a new wire that would go past it is refused. Wires already in the drawing are never touched. - Every rule is off unless the project turns it on, and the application + The rules are set once for the application (Settings > General, + applicationSettings()) and apply to every project; a project can set + its own instead (Settings::own), saved in the project so it travels + with the drawing. Every rule is off by default, and the application has a master switch (masterEnabled()) that turns all of them off in every project at once. Kept free of any QGraphicsItem so the rules can be tested on their own. @@ -38,10 +41,17 @@ namespace WiringRules { ///The QSettings key of the master switch constexpr const char *settings_key = "diagrameditor/wiring_rules_enabled"; + ///The QSettings keys of the application's rules + constexpr const char *max_wires_key = "diagrameditor/wiring_rules_max_wires"; + constexpr const char *one_wire_per_report_key = "diagrameditor/wiring_rules_one_wire_per_report"; - ///The rules a project chose. The default is "no rule". + ///A set of rules: the application's, or a project's. The default is + ///"no rule". struct Settings { + ///For a project: it uses these values rather than the + ///application's. Ignored in the application's own settings. + bool own = false; ///Most wires a terminal may take, 0 for no limit int max_wires = 0; ///A folio report takes one wire only @@ -49,7 +59,8 @@ namespace WiringRules bool isDefault() const { return *this == Settings(); } bool operator==(const Settings &other) const { - return max_wires == other.max_wires + return own == other.own + && max_wires == other.max_wires && one_wire_per_report == other.one_wire_per_report; } bool operator!=(const Settings &other) const { return !(*this == other); } @@ -58,6 +69,10 @@ namespace WiringRules bool masterEnabled(); void setMasterEnabled(bool enabled); + Settings applicationSettings(); + void setApplicationSettings(const Settings &settings); + Settings effective(const Settings &project, const Settings &application); + Settings fromXml(const QDomElement &project_root); void toXml(const Settings &settings, QDomElement &project_root); diff --git a/tests/qttest/tst_wiringrules.cpp b/tests/qttest/tst_wiringrules.cpp index 79353cf26..b53b6073e 100644 --- a/tests/qttest/tst_wiringrules.cpp +++ b/tests/qttest/tst_wiringrules.cpp @@ -23,8 +23,9 @@ class tst_wiringrules : public QObject int m_run = 0; // Environment of one sandboxed run; @p master_off writes the master - // switch off into that run's own settings. - QProcessEnvironment sandbox(bool master_off = false) + // switch off into that run's own settings, @p application_max_wires + // the limit every project follows unless it sets its own. + QProcessEnvironment sandbox(bool master_off = false, int application_max_wires = 0) { const QString home = m_dir.filePath(QStringLiteral("home%1").arg(m_run)); const QString tmp = m_dir.filePath(QStringLiteral("tmp%1").arg(m_run)); @@ -33,10 +34,12 @@ class tst_wiringrules : public QObject QDir().mkpath(home); QDir().mkpath(tmp); QDir().mkpath(settings + QStringLiteral("/QElectroTech")); - if (master_off) { + if (master_off || application_max_wires) { QFile ini(settings + QStringLiteral("/QElectroTech/QElectroTech.ini")); if (ini.open(QIODevice::WriteOnly | QIODevice::Text)) - ini.write("[diagrameditor]\nwiring_rules_enabled=false\n"); + ini.write(QStringLiteral("[diagrameditor]\nwiring_rules_enabled=%1\nwiring_rules_max_wires=%2\n") + .arg(master_off ? QStringLiteral("false") : QStringLiteral("true")) + .arg(application_max_wires).toUtf8()); } QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen")); @@ -85,7 +88,8 @@ class tst_wiringrules : public QObject // Wires a free terminal to a terminal that already has a wire, through // qet.addConductor(); returns what the script printed after "PROBE ". - QString addWireToWiredTerminal(const QString &project, bool master_off) + QString addWireToWiredTerminal(const QString &project, bool master_off, + int application_max_wires = 0) { const QString script_path = m_dir.filePath(QStringLiteral("probe%1.js").arg(m_run)); QFile script(script_path); @@ -109,7 +113,7 @@ class tst_wiringrules : public QObject script.close(); QProcess proc; - proc.setProcessEnvironment(sandbox(master_off)); + proc.setProcessEnvironment(sandbox(master_off, application_max_wires)); proc.start(QStringLiteral(QET_TEST_BINARY_PATH), {QStringLiteral("--run"), script_path, project}); if (!proc.waitForFinished(60000)) return {}; @@ -160,18 +164,52 @@ private slots: QVERIFY(!WiringRules::hasRoom(1, 1)); } + void projectOverridesApplication() + { + WiringRules::Settings application; + application.max_wires = 2; + application.one_wire_per_report = true; + + // A project that follows the application gets its rules + WiringRules::Settings follows; + QCOMPARE(WiringRules::effective(follows, application).max_wires, 2); + QVERIFY(WiringRules::effective(follows, application).one_wire_per_report); + QVERIFY(!WiringRules::effective(follows, application).own); + + // Its own rules win, "no limit" included + WiringRules::Settings own; + own.own = true; + QCOMPARE(WiringRules::effective(own, application).max_wires, 0); + QVERIFY(!WiringRules::effective(own, application).one_wire_per_report); + QVERIFY(WiringRules::effective(own, application).own); + own.max_wires = 6; + QCOMPARE(WiringRules::effective(own, application).max_wires, 6); + } + void xmlRoundTrip() { QDomDocument doc; QDomElement root = doc.createElement(QStringLiteral("project")); doc.appendChild(root); - // No rule: nothing written, and reading nothing gives no rule - WiringRules::toXml(WiringRules::Settings(), root); + // Following the application: nothing written, and reading + // nothing follows the application + WiringRules::Settings follows; + follows.max_wires = 3; // not the project's: not written + WiringRules::toXml(follows, root); QVERIFY(root.firstChildElement().isNull()); QVERIFY(WiringRules::fromXml(root).isDefault()); + // Its own rules, even "none", are written and read back as its own + WiringRules::Settings none; + none.own = true; + WiringRules::toXml(none, root); + QCOMPARE(root.firstChildElement().tagName(), QStringLiteral("wiring_rules")); + QVERIFY(WiringRules::fromXml(root) == none); + root.removeChild(root.firstChildElement()); + WiringRules::Settings rules; + rules.own = true; rules.max_wires = 2; rules.one_wire_per_report = true; WiringRules::toXml(rules, root); @@ -193,6 +231,14 @@ private slots: QVERIFY2(saved.contains(QLatin1String("max_wires_per_terminal=\"2\"")) && saved.contains(QLatin1String("one_wire_per_report=\"true\"")), "the setting was lost on save"); + + // A project's own "no rule" is kept too: it overrides the application's + const QString own_none = fixtureWith(QStringLiteral("")); + QVERIFY(!own_none.isEmpty()); + const QString saved_none = resave(own_none); + QVERIFY2(!saved_none.isEmpty(), "--resave failed"); + QVERIFY2(saved_none.contains(QLatin1String("")), + "a project's own rules were lost on save"); } void wirePastTheLimitIsRefused() @@ -209,6 +255,16 @@ private slots: // The master switch off: the project's rule does nothing QCOMPARE(addWireToWiredTerminal(limited, true), QStringLiteral("true")); + + // The application's limit applies to a project that sets none... + const QString plain = QFINDTESTDATA("fixtures/qet_bug_repro_resaved.qet"); + QCOMPARE(addWireToWiredTerminal(plain, false, 1), QStringLiteral("false")); + // ...not to one that sets its own, here "no limit" + const QString own_none = fixtureWith(QStringLiteral("")); + QVERIFY(!own_none.isEmpty()); + QCOMPARE(addWireToWiredTerminal(own_none, false, 1), QStringLiteral("true")); + // ...and the master switch still turns it off + QCOMPARE(addWireToWiredTerminal(plain, true, 1), QStringLiteral("true")); } };