From 20e134f2bd86effb36067696a12e63ee7cb00c23 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Sat, 3 Oct 2026 21:42:12 +1300 Subject: [PATCH 1/6] Add a wires-per-terminal limit, with an application master switch Discussion #1158: a real terminal takes one or two wires (four with double ferrules), a folio report is a virtual point that carries one, but QElectroTech lets any number of wires be connected anywhere. Project properties > General gets a "Conducteurs par borne" group: - the most wires a terminal may take (0, the default, is no limit); - "one wire per folio report". When set, a new wire that would go past the limit is refused: the terminal shows red while dragging, as for any refused link, and a tooltip says why on release. Auto-connect on placing, moving or multi-pasting a symbol skips full terminals, and qet.addConductor() refuses with its own message. Wires already drawn, opening, pasting and undo are never refused. Settings > General gets a master switch, on by default, that turns every rule of this feature off in every project; the project group is greyed with a note while it is off. The rules live in wiringrules.cpp. The project setting is saved as only when a rule is on, so a project that never used it saves exactly as before; an older version drops it, which means "off". tst_wiringrules covers the rules, the save round trip and, through the real binary, that a wire past the limit is refused unless the master switch is off (checked to fail with the limit check removed). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015FPuYPS4T7QuEwjNu22rXD --- cmake/qet_compilation_vars.cmake | 2 + sources/qetgraphicsitem/element.cpp | 7 +- sources/qetgraphicsitem/terminal.cpp | 52 ++++- sources/qetgraphicsitem/terminal.h | 2 + sources/qetproject.cpp | 43 ++++ sources/qetproject.h | 6 + sources/scripting/qetscriptapi.cpp | 5 + .../configpage/generalconfigurationpage.cpp | 3 + .../ui/configpage/generalconfigurationpage.ui | 14 ++ sources/ui/configpage/projectconfigpages.cpp | 51 ++++ sources/ui/configpage/projectconfigpages.h | 7 + sources/wiringrules.cpp | 126 ++++++++++ sources/wiringrules.h | 68 ++++++ tests/qttest/CMakeLists.txt | 15 ++ tests/qttest/tst_wiringrules.cpp | 217 ++++++++++++++++++ 15 files changed, 615 insertions(+), 3 deletions(-) create mode 100644 sources/wiringrules.cpp create mode 100644 sources/wiringrules.h create mode 100644 tests/qttest/tst_wiringrules.cpp diff --git a/cmake/qet_compilation_vars.cmake b/cmake/qet_compilation_vars.cmake index efe2f1992..9d6e2ef3d 100644 --- a/cmake/qet_compilation_vars.cmake +++ b/cmake/qet_compilation_vars.cmake @@ -179,6 +179,8 @@ set(QET_SRC_FILES ${QET_DIR}/sources/wiringlistexport.cpp ${QET_DIR}/sources/wirehops.h ${QET_DIR}/sources/wirehops.cpp + ${QET_DIR}/sources/wiringrules.h + ${QET_DIR}/sources/wiringrules.cpp ${QET_DIR}/sources/conductorrouter.h ${QET_DIR}/sources/conductorrouter.cpp ${QET_DIR}/sources/ui/wiringlistdialog.h diff --git a/sources/qetgraphicsitem/element.cpp b/sources/qetgraphicsitem/element.cpp index a91e78433..77507f3da 100644 --- a/sources/qetgraphicsitem/element.cpp +++ b/sources/qetgraphicsitem/element.cpp @@ -1345,7 +1345,8 @@ bool Element::removeTextFromGroup(DynamicElementTextItem *text, The first Terminal of QPair is a Terminal owned by this element, this terminal haven't got any conductor docked. The second Terminal of QPair is a Terminal owned by an other element, - which is aligned with the first Terminal. The second Terminal can have or not docked conductors. + which is aligned with the first Terminal. The second Terminal can have or not docked conductors, + but no more than the project's wires-per-terminal limit allows (discussion #1158). */ QList > Element::AlignedFreeTerminals() const { @@ -1357,7 +1358,9 @@ QList > Element::AlignedFreeTerminals() const { Terminal *other_terminal = terminal -> alignedWithTerminal(); - if (other_terminal) + if (other_terminal + && terminal->hasRoomForWire() + && other_terminal->hasRoomForWire()) list << qMakePair(terminal, other_terminal); } } diff --git a/sources/qetgraphicsitem/terminal.cpp b/sources/qetgraphicsitem/terminal.cpp index e3ba0198d..e9e114a05 100644 --- a/sources/qetgraphicsitem/terminal.cpp +++ b/sources/qetgraphicsitem/terminal.cpp @@ -26,6 +26,9 @@ #include "../qetgraphicsitem/conductor.h" #include "../qetgraphicsitem/element.h" #include "conductortextitem.h" +#include "../wiringrules.h" + +#include #include #include @@ -610,7 +613,22 @@ void Terminal::mouseReleaseEvent(QGraphicsSceneMouseEvent *e) other_terminal -> m_hovered = false; //We stop her if we can't link this terminal with other terminal - if (!canBeLinkedTo(other_terminal)) return; + if (!canBeLinkedTo(other_terminal)) + { + //Say why when it is the wire limit (discussion #1158): a red + //terminal alone does not tell the user what to do instead. + const Terminal *full = !hasRoomForWire() ? this + : !other_terminal->hasRoomForWire() ? other_terminal + : nullptr; + if (full) { + QToolTip::showText(e->screenPos(), + tr("Cette borne a déjà %n conducteur(s), la limite du projet. " + "Ajoutez une borne pour raccorder un conducteur de plus.", + "wire refused by the wires-per-terminal limit", + full->wireLimit())); + } + return; + } //Create conductor Conductor *new_conductor = new Conductor(this, other_terminal); @@ -692,6 +710,7 @@ bool Terminal::isLinkedTo(Terminal *other_terminal) { Reasons for not linable: - \p other_terminal is this terminal - this terminal is already connected to \p other_terminal + - either terminal already has as many wires as the project allows @param other_terminal @return true if this terminal can be linked to other_terminal, otherwise false @@ -701,9 +720,40 @@ bool Terminal::canBeLinkedTo(Terminal *other_terminal) if (other_terminal == this || isLinkedTo(other_terminal)) return false; + if (!hasRoomForWire() || !other_terminal->hasRoomForWire()) + return false; + return true; } +/** + @brief Terminal::wireLimit + @return the most wires this terminal may take under its project's + wiring rules (discussion #1158), 0 for no limit. +*/ +int Terminal::wireLimit() const +{ + const Diagram *parent_diagram = diagram(); + if (!parent_diagram || !parent_diagram->project()) { + return 0; + } + const Element *element = parentElement(); + const bool is_report = element + && (element->linkType() & (Element::NextReport | Element::PreviousReport)); + return WiringRules::limit(parent_diagram->project()->wiringRules(), + WiringRules::masterEnabled(), + is_report); +} + +/** + @brief Terminal::hasRoomForWire + @return true if one more wire may be connected to this terminal +*/ +bool Terminal::hasRoomForWire() const +{ + return WiringRules::hasRoom(wireLimit(), conductorsCount()); +} + /** @brief Terminal::conductors @return La liste des conducteurs lies a cette borne diff --git a/sources/qetgraphicsitem/terminal.h b/sources/qetgraphicsitem/terminal.h index b491f1ec2..419f4cdec 100644 --- a/sources/qetgraphicsitem/terminal.h +++ b/sources/qetgraphicsitem/terminal.h @@ -92,6 +92,8 @@ class Terminal : public QGraphicsObject void updateConductor(); bool isLinkedTo(Terminal *); bool canBeLinkedTo(Terminal *); + int wireLimit() const; + bool hasRoomForWire() const; // methods related to XML import/export static bool valideXml(QDomElement &); diff --git a/sources/qetproject.cpp b/sources/qetproject.cpp index 21c6f83ce..4a20fd926 100644 --- a/sources/qetproject.cpp +++ b/sources/qetproject.cpp @@ -1257,6 +1257,26 @@ 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. +*/ +WiringRules::Settings QETProject::wiringRules() 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. + @param rules +*/ +void QETProject::setWiringRules(const WiringRules::Settings &rules) +{ + m_wiring_rules = rules; +} + /** @brief QETProject::autoBreakConductor @return true if use of auto break conductor is authorized. @@ -1367,6 +1387,7 @@ QDomDocument QETProject::toXml() // local, non-transmitted usage tracking (time spent on this project) writeUsageXml(project_root); writeWireHopsXml(project_root); + writeWiringRulesXml(project_root); // Properties for news diagrams QDomElement new_diagrams_properties = xml_doc.createElement("newdiagrams"); @@ -1877,6 +1898,7 @@ void QETProject::readProjectXml(QDomDocument &xml_project) //Load the local, non-transmitted usage tracking readUsageXml(xml_project); readWireHopsXml(xml_project); + readWiringRulesXml(xml_project); //Load the default properties for the new diagrams readDefaultPropertiesXml(xml_project); @@ -2070,6 +2092,17 @@ void QETProject::readWireHopsXml(QDomDocument &xml_project) : WireHops::fromString(crossings.attribute(QStringLiteral("hop"))); } +/** + @brief QETProject::readWiringRulesXml + Read the element of the project, if any. + A project without it sets no rule. + @param xml_project : the xml description of the project +*/ +void QETProject::readWiringRulesXml(QDomDocument &xml_project) +{ + m_wiring_rules = WiringRules::fromXml(xml_project.documentElement()); +} + /** @brief QETProject::readDefaultPropertiesXml load default properties for new diagram, found in the xml of this project @@ -2232,6 +2265,16 @@ void QETProject::writeWireHopsXml(QDomElement &xml_element) xml_element.appendChild(crossings); } +/** + @brief QETProject::writeWiringRulesXml + Export the project's wiring rules as a child of + \a xml_element, only when one is set. +*/ +void QETProject::writeWiringRulesXml(QDomElement &xml_element) +{ + WiringRules::toXml(m_wiring_rules, xml_element); +} + /** @brief QETProject::writeDefaultPropertiesXml Export all defaults properties used by a new diagram and his content diff --git a/sources/qetproject.h b/sources/qetproject.h index d2fe564f2..2f713919e 100644 --- a/sources/qetproject.h +++ b/sources/qetproject.h @@ -29,6 +29,7 @@ #include "titleblock/templatescollection.h" #include "titleblockproperties.h" #include "wirehops.h" +#include "wiringrules.h" #include "diagram.h" #ifdef BUILD_WITHOUT_KF # include "ui/nokde/kautosavefile.h" @@ -211,6 +212,8 @@ class QETProject : public QObject void setAutoConductor (bool ac); WireHops::Mode wireHops() const; void setWireHops(WireHops::Mode mode); + WiringRules::Settings wiringRules() const; + void setWiringRules(const WiringRules::Settings &rules); void setAutoBreakConductor (bool abc); void setAutoElement (bool ae); void autoFolioNumberingNewFolios (); @@ -308,11 +311,13 @@ class QETProject : public QObject void readTerminalStripXml(const QDomDocument &xml_project); void readUsageXml(QDomDocument &xml_project); void readWireHopsXml(QDomDocument &xml_project); + void readWiringRulesXml(QDomDocument &xml_project); void writeProjectPropertiesXml(QDomElement &); void writeDefaultPropertiesXml(QDomElement &); void writeUsageXml(QDomElement &); void writeWireHopsXml(QDomElement &); + void writeWiringRulesXml(QDomElement &); void addDiagram(Diagram *diagram, int pos = -1); void detachDiagram(Diagram *diagram); void writeBackup(); @@ -371,6 +376,7 @@ class QETProject : public QObject QString m_current_element_autonum; bool m_auto_conductor = true; WireHops::Mode m_wire_hops = WireHops::Mode::None; + WiringRules::Settings m_wiring_rules; bool m_auto_break_conductor = false; XmlElementCollection *m_elements_collection = nullptr; bool m_freeze_new_elements = false; diff --git a/sources/scripting/qetscriptapi.cpp b/sources/scripting/qetscriptapi.cpp index d61768793..eea431088 100644 --- a/sources/scripting/qetscriptapi.cpp +++ b/sources/scripting/qetscriptapi.cpp @@ -963,6 +963,11 @@ bool QetScriptApi::addConductor(int folioIndex, log(QStringLiteral("qet.addConductor: those two terminals are already wired together")); return false; } + if (!t1->hasRoomForWire() || !t2->hasRoomForWire()) { + log(QStringLiteral("qet.addConductor: a terminal already has as many wires as the " + "project's wires-per-terminal limit allows")); + return false; + } if (!t1->canBeLinkedTo(t2)) { log(QStringLiteral("qet.addConductor: those two terminals cannot be linked")); return false; diff --git a/sources/ui/configpage/generalconfigurationpage.cpp b/sources/ui/configpage/generalconfigurationpage.cpp index 1a1f86811..7a6293b81 100644 --- a/sources/ui/configpage/generalconfigurationpage.cpp +++ b/sources/ui/configpage/generalconfigurationpage.cpp @@ -27,6 +27,7 @@ #include "../../utils/qetutils.h" #include "../../qetmessagebox.h" #include "../../textgrid.h" +#include "../../wiringrules.h" #include "../../editor/terminalnamecheck.h" #include "../../ElementsCollection/qetlabelsfile.h" #include "../prefixconfigurationdialog.h" @@ -113,6 +114,7 @@ GeneralConfigurationPage::GeneralConfigurationPage(QWidget *parent) : ui->m_use_windows_mode_rb->setChecked(true); 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()); 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()); @@ -334,6 +336,7 @@ void GeneralConfigurationPage::applyConf() settings.setValue("diagrameditor/highlight-integrated-elements", ui->m_highlight_integrated_elements->isChecked()); 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()); 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 bc60c7c27..adf7e3a21 100644 --- a/sources/ui/configpage/generalconfigurationpage.ui +++ b/sources/ui/configpage/generalconfigurationpage.ui @@ -70,6 +70,19 @@ + + + + Décoché, aucune règle de conducteurs par borne n'est appliquée, quels que soient les réglages des projets. + + + Appliquer les règles de conducteurs par borne des projets + + + true + + + @@ -1297,6 +1310,7 @@ Vous pouvez spécifier ici la valeur par défaut de ce champ pour les éléments m_use_gesture_trackpad m_zoom_out_beyond_folio m_conductor_properties_panel + m_wiring_rules_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 feae7a034..d1ac9520d 100644 --- a/sources/ui/configpage/projectconfigpages.cpp +++ b/sources/ui/configpage/projectconfigpages.cpp @@ -26,6 +26,7 @@ #include "../qet.h" #include "../qeticons.h" #include "../qetproject.h" +#include "../wiringrules.h" #include "../borderpropertieswidget.h" #include "../conductorpropertieswidget.h" #include "../diagramcontextwidget.h" @@ -176,6 +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) { + m_project -> setWiringRules(wiring_rules); + modified_project = true; + } + if (modified_project) { m_project -> setModified(true); } @@ -225,6 +234,25 @@ void ProjectMainConfigPage::initWidgets() wire_hops_cb_ -> setToolTip(tr("Dessine un petit arc là où deux conducteurs se croisent sans être reliés. " "Seul le dessin change : aucun élément n'est ajouté et aucun conducteur n'est coupé.", "tooltip")); + + //How many wires a terminal may take (discussion #1158) + wiring_rules_gb_ = new QGroupBox(tr("Conducteurs par borne", "group box title")); + 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); + max_wires_sb_ -> setSpecialValueText(tr("Sans limite", "wires per terminal")); + max_wires_sb_ -> setToolTip(tr("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.", + "tooltip")); + one_wire_per_report_cb_ = new QCheckBox(tr("Un seul conducteur par renvoi de folio", "checkbox label")); + one_wire_per_report_cb_ -> setToolTip(tr("Un renvoi de folio est un point virtuel : il ne reçoit qu'un conducteur, " + "celui qui continue sur l'autre folio.", + "tooltip")); + wiring_rules_off_label_ = new QLabel(tr("Ces règles sont désactivées pour tous les projets " + "(Configurer QElectroTech > Général).", + "informative label")); + wiring_rules_off_label_ -> setWordWrap(true); } /** @@ -257,6 +285,17 @@ void ProjectMainConfigPage::initLayout() wire_hops_layout0 -> addWidget(wire_hops_cb_); wire_hops_layout0 -> addStretch(); main_layout0 -> addLayout(wire_hops_layout0); + main_layout0 -> addSpacing(10); + + QVBoxLayout *wiring_rules_layout = new QVBoxLayout(wiring_rules_gb_); + QHBoxLayout *max_wires_layout = new QHBoxLayout(); + max_wires_layout -> addWidget(max_wires_label_); + max_wires_layout -> addWidget(max_wires_sb_); + max_wires_layout -> addStretch(); + wiring_rules_layout -> addLayout(max_wires_layout); + wiring_rules_layout -> addWidget(one_wire_per_report_cb_); + wiring_rules_layout -> addWidget(wiring_rules_off_label_); + main_layout0 -> addWidget(wiring_rules_gb_); setLayout(main_layout0); this -> setMinimumWidth(680); @@ -280,6 +319,17 @@ 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)); + + const WiringRules::Settings wiring_rules = m_project -> wiringRules(); + max_wires_sb_ -> setValue(wiring_rules.max_wires); + one_wire_per_report_cb_ -> setChecked(wiring_rules.one_wire_per_report); + //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(); + wiring_rules_off_label_ -> setVisible(!master); + max_wires_label_ -> setEnabled(master); + max_wires_sb_ -> setEnabled(master); + one_wire_per_report_cb_ -> setEnabled(master); } /** @@ -307,6 +357,7 @@ void ProjectMainConfigPage::adjustReadOnly() usage_enabled_cb_ -> setDisabled(is_read_only); usage_reset_pb_ -> setDisabled(is_read_only); wire_hops_cb_ -> setDisabled(is_read_only); + wiring_rules_gb_ -> setDisabled(is_read_only); } //######################################################################################// diff --git a/sources/ui/configpage/projectconfigpages.h b/sources/ui/configpage/projectconfigpages.h index 7375d83ff..0868815d1 100644 --- a/sources/ui/configpage/projectconfigpages.h +++ b/sources/ui/configpage/projectconfigpages.h @@ -24,6 +24,8 @@ class QComboBox; class QLineEdit; class QCheckBox; class QPushButton; +class QSpinBox; +class QGroupBox; class QETProject; class BorderPropertiesWidget; class ConductorPropertiesWidget; @@ -128,6 +130,11 @@ class ProjectMainConfigPage : public ProjectConfigPage { QPushButton *usage_reset_pb_; QLabel *wire_hops_label_; QComboBox *wire_hops_cb_; + QGroupBox *wiring_rules_gb_; + QLabel *max_wires_label_; + QSpinBox *max_wires_sb_; + QCheckBox *one_wire_per_report_cb_; + QLabel *wiring_rules_off_label_; }; class ProjectAutoNumConfigPage : public ProjectConfigPage { diff --git a/sources/wiringrules.cpp b/sources/wiringrules.cpp new file mode 100644 index 000000000..fcc3a66a2 --- /dev/null +++ b/sources/wiringrules.cpp @@ -0,0 +1,126 @@ +/* + 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 . +*/ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "wiringrules.h" + +#include +#include +#include + +namespace { + const QString element_name = QStringLiteral("wiring_rules"); + const QString max_wires_attribute = QStringLiteral("max_wires_per_terminal"); + const QString report_attribute = QStringLiteral("one_wire_per_report"); + + //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; +} + +/** + @brief WiringRules::masterEnabled + @return false when the user turned every rule of this feature off for + every project (Settings > General). On by default: a project still has + to turn a rule on before anything changes. +*/ +bool WiringRules::masterEnabled() +{ + if (master_cache < 0) { + master_cache = QSettings().value(settings_key, true).toBool() ? 1 : 0; + } + return master_cache == 1; +} + +/** + @brief WiringRules::setMasterEnabled + Save the master switch. + @param enabled +*/ +void WiringRules::setMasterEnabled(bool enabled) +{ + QSettings().setValue(settings_key, enabled); + master_cache = enabled ? 1 : 0; +} + +/** + @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. +*/ +WiringRules::Settings WiringRules::fromXml(const QDomElement &project_root) +{ + Settings settings; + const QDomElement rules = project_root.firstChildElement(element_name); + if (rules.isNull()) { + return settings; + } + 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; +} + +/** + @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. +*/ +void WiringRules::toXml(const Settings &settings, QDomElement &project_root) +{ + if (settings.isDefault()) { + return; + } + QDomElement rules = project_root.ownerDocument().createElement(element_name); + if (settings.max_wires > 0) { + rules.setAttribute(max_wires_attribute, settings.max_wires); + } + if (settings.one_wire_per_report) { + rules.setAttribute(report_attribute, QStringLiteral("true")); + } + project_root.appendChild(rules); +} + +/** + @brief WiringRules::limit + @param settings : the project's rules + @param master_enabled : the master switch, masterEnabled() + @param is_report : the terminal belongs to a folio report + @return the most wires the terminal may take, 0 for no limit. +*/ +int WiringRules::limit(const Settings &settings, bool master_enabled, bool is_report) +{ + if (!master_enabled) { + return 0; + } + if (is_report && settings.one_wire_per_report) { + return 1; + } + return settings.max_wires; +} + +/** + @brief WiringRules::hasRoom + @param limit : WiringRules::limit() + @param wires : the wires the terminal already has + @return true if one more wire may be connected +*/ +bool WiringRules::hasRoom(int limit, int wires) +{ + return limit <= 0 || wires < limit; +} diff --git a/sources/wiringrules.h b/sources/wiringrules.h new file mode 100644 index 000000000..f9cab9247 --- /dev/null +++ b/sources/wiringrules.h @@ -0,0 +1,68 @@ +/* + 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 . +*/ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef WIRINGRULES_H +#define WIRINGRULES_H + +class QDomElement; + +/** + @brief The WiringRules namespace + How many wires a terminal may take (discussion #1158). A real terminal + takes one or two wires, sometimes four with double ferrules; a folio + report is a virtual point that should carry one. When a project sets a + 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 + 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. +*/ +namespace WiringRules +{ + ///The QSettings key of the master switch + constexpr const char *settings_key = "diagrameditor/wiring_rules_enabled"; + + ///The rules a project chose. The default is "no rule". + struct Settings + { + ///Most wires a terminal may take, 0 for no limit + int max_wires = 0; + ///A folio report takes one wire only + bool one_wire_per_report = false; + + bool isDefault() const { return *this == Settings(); } + bool operator==(const Settings &other) const { + return max_wires == other.max_wires + && one_wire_per_report == other.one_wire_per_report; + } + bool operator!=(const Settings &other) const { return !(*this == other); } + }; + + bool masterEnabled(); + void setMasterEnabled(bool enabled); + + Settings fromXml(const QDomElement &project_root); + void toXml(const Settings &settings, QDomElement &project_root); + + int limit(const Settings &settings, bool master_enabled, bool is_report); + bool hasRoom(int limit, int wires); +} + +#endif // WIRINGRULES_H diff --git a/tests/qttest/CMakeLists.txt b/tests/qttest/CMakeLists.txt index fb57677c5..164f6cfc0 100644 --- a/tests/qttest/CMakeLists.txt +++ b/tests/qttest/CMakeLists.txt @@ -460,6 +460,21 @@ target_link_libraries(tst_wirehops PRIVATE Qt::Test Qt::Gui) target_compile_definitions(tst_wirehops PRIVATE "QET_TEST_BINARY_PATH=\"$\"") +# Wires per terminal (discussion #1158): the rules in wiringrules.cpp, and +# through the real binary, that --resave keeps the project setting, adds +# none to a project without it, and that a wire past the limit is refused +# unless the master switch is off. +add_executable( + tst_wiringrules + tst_wiringrules.cpp + ${QET_DIR}/sources/wiringrules.cpp) +add_test(NAME tst_wiringrules COMMAND tst_wiringrules) +add_dependencies(tst_wiringrules qelectrotech) +target_include_directories(tst_wiringrules PRIVATE ${QET_DIR}/sources) +target_link_libraries(tst_wiringrules PRIVATE Qt::Test Qt::Xml) +target_compile_definitions(tst_wiringrules PRIVATE + "QET_TEST_BINARY_PATH=\"$\"") + # The routing behind qet.routeConductor(): conductorrouter.cpp needs only # Qt Core, so the geometry is tested without a scene. add_executable( diff --git a/tests/qttest/tst_wiringrules.cpp b/tests/qttest/tst_wiringrules.cpp new file mode 100644 index 000000000..79353cf26 --- /dev/null +++ b/tests/qttest/tst_wiringrules.cpp @@ -0,0 +1,217 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include + +#include +#include +#include +#include +#include +#include + +#include "wiringrules.h" + +// How many wires a terminal may take (discussion #1158). The rules are +// tested on their own; the project setting and the refusal are tested +// through the real binary: --resave keeps a element and +// adds none to a project without it, and qet.addConductor() is refused +// past the limit unless the master switch is off. +class tst_wiringrules : public QObject +{ + Q_OBJECT + + QTemporaryDir m_dir; + 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) + { + const QString home = m_dir.filePath(QStringLiteral("home%1").arg(m_run)); + const QString tmp = m_dir.filePath(QStringLiteral("tmp%1").arg(m_run)); + const QString settings = m_dir.filePath(QStringLiteral("settings%1").arg(m_run)); + ++m_run; + QDir().mkpath(home); + QDir().mkpath(tmp); + QDir().mkpath(settings + QStringLiteral("/QElectroTech")); + if (master_off) { + QFile ini(settings + QStringLiteral("/QElectroTech/QElectroTech.ini")); + if (ini.open(QIODevice::WriteOnly | QIODevice::Text)) + ini.write("[diagrameditor]\nwiring_rules_enabled=false\n"); + } + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen")); + env.insert(QStringLiteral("QET_ENABLE_SCRIPTING"), QStringLiteral("1")); + env.insert(QStringLiteral("HOME"), home); + env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/.config")); + env.insert(QStringLiteral("XDG_DATA_HOME"), home + QStringLiteral("/.local/share")); + env.insert(QStringLiteral("QET_SETTINGS_DIR"), settings); + env.insert(QStringLiteral("TMPDIR"), tmp); + return env; + } + + // The fixture with @p rules inserted before , as a new file + QString fixtureWith(const QString &rules) + { + QFile source(QFINDTESTDATA("fixtures/qet_bug_repro_resaved.qet")); + if (!source.open(QIODevice::ReadOnly | QIODevice::Text)) + return {}; + QString text = QString::fromUtf8(source.readAll()); + const int newdiagrams = text.indexOf(QLatin1String("= 0) + return line.mid(i + mark.size()).trimmed(); + } + return {}; + } + +private slots: + void initTestCase() + { + QVERIFY(m_dir.isValid()); + QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH))); + } + + void limit() + { + WiringRules::Settings rules; + QCOMPARE(WiringRules::limit(rules, true, false), 0); + QCOMPARE(WiringRules::limit(rules, true, true), 0); + + rules.max_wires = 4; + QCOMPARE(WiringRules::limit(rules, true, false), 4); + QCOMPARE(WiringRules::limit(rules, true, true), 4); + + rules.one_wire_per_report = true; + QCOMPARE(WiringRules::limit(rules, true, true), 1); + QCOMPARE(WiringRules::limit(rules, true, false), 4); + + // The master switch turns every rule off + QCOMPARE(WiringRules::limit(rules, false, false), 0); + QCOMPARE(WiringRules::limit(rules, false, true), 0); + } + + void hasRoom() + { + QVERIFY(WiringRules::hasRoom(0, 0)); + QVERIFY(WiringRules::hasRoom(0, 19)); + QVERIFY(WiringRules::hasRoom(2, 1)); + QVERIFY(!WiringRules::hasRoom(2, 2)); + QVERIFY(!WiringRules::hasRoom(2, 3)); + QVERIFY(!WiringRules::hasRoom(1, 1)); + } + + 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); + QVERIFY(root.firstChildElement().isNull()); + QVERIFY(WiringRules::fromXml(root).isDefault()); + + WiringRules::Settings rules; + rules.max_wires = 2; + rules.one_wire_per_report = true; + WiringRules::toXml(rules, root); + QCOMPARE(root.firstChildElement().tagName(), QStringLiteral("wiring_rules")); + QVERIFY(WiringRules::fromXml(root) == rules); + } + + void savedSettingSurvivesResave() + { + const QString plain = resave(QFINDTESTDATA("fixtures/qet_bug_repro_resaved.qet")); + QVERIFY2(!plain.isEmpty(), "--resave failed"); + QVERIFY(!plain.contains(QLatin1String("wiring_rules"))); + + const QString with_rules = fixtureWith( + QStringLiteral("")); + QVERIFY(!with_rules.isEmpty()); + const QString saved = resave(with_rules); + QVERIFY2(!saved.isEmpty(), "--resave failed"); + QVERIFY2(saved.contains(QLatin1String("max_wires_per_terminal=\"2\"")) + && saved.contains(QLatin1String("one_wire_per_report=\"true\"")), + "the setting was lost on save"); + } + + void wirePastTheLimitIsRefused() + { + // Without a rule the wire is drawn, as on master + QCOMPARE(addWireToWiredTerminal(QFINDTESTDATA("fixtures/qet_bug_repro_resaved.qet"), false), + QStringLiteral("true")); + + // One wire per terminal: the terminal is full + const QString limited = fixtureWith( + QStringLiteral("")); + QVERIFY(!limited.isEmpty()); + QCOMPARE(addWireToWiredTerminal(limited, false), QStringLiteral("false")); + + // The master switch off: the project's rule does nothing + QCOMPARE(addWireToWiredTerminal(limited, true), QStringLiteral("true")); + } +}; + +QTEST_GUILESS_MAIN(tst_wiringrules) + +#include "tst_wiringrules.moc" From 7ba820f8f0b6d7fec3b29776a915876c78e6b5b3 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Sat, 3 Oct 2026 23:16:43 +1300 Subject: [PATCH 2/6] 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")); } }; From efd37538e03f9b0cb9d22e5841d861529366ae37 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Sat, 3 Oct 2026 23:51:14 +1300 Subject: [PATCH 3/6] Settings > General: widen the wire limit box so "Sans limite" fits Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015FPuYPS4T7QuEwjNu22rXD --- sources/ui/configpage/generalconfigurationpage.ui | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/sources/ui/configpage/generalconfigurationpage.ui b/sources/ui/configpage/generalconfigurationpage.ui index 1120faa20..427c9fc7b 100644 --- a/sources/ui/configpage/generalconfigurationpage.ui +++ b/sources/ui/configpage/generalconfigurationpage.ui @@ -97,6 +97,12 @@ + + + 120 + 0 + + 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. From 5db92384a5e8a4eef703d1e08f52cb2469fda8d8 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Sun, 4 Oct 2026 00:24:20 +1300 Subject: [PATCH 4/6] tst_wiringrules: skip the --run checks on a build without scripting The project's CI builds QElectroTech without scripting, where --run is not an option and the process never exits, so the test waited out its timeout and failed (#1272's stack, CI run 37118548599). The --run checks now skip there, as the script tests are left out of such builds; the rules and the save round trip still run. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015FPuYPS4T7QuEwjNu22rXD --- tests/qttest/tst_wiringrules.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/qttest/tst_wiringrules.cpp b/tests/qttest/tst_wiringrules.cpp index b53b6073e..f7f258d68 100644 --- a/tests/qttest/tst_wiringrules.cpp +++ b/tests/qttest/tst_wiringrules.cpp @@ -10,6 +10,15 @@ #include "wiringrules.h" + // The checks that drive QElectroTech through a script (--run) need a + // build with scripting; without it --run is not an option and the + // process never exits (QET_HAS_SCRIPTING, top-level CMakeLists.txt). +#ifdef QET_HAS_SCRIPTING +# define SKIP_WITHOUT_SCRIPTING +#else +# define SKIP_WITHOUT_SCRIPTING QSKIP("built without scripting: --run is not available") +#endif + // How many wires a terminal may take (discussion #1158). The rules are // tested on their own; the project setting and the refusal are tested // through the real binary: --resave keeps a element and @@ -243,6 +252,7 @@ private slots: void wirePastTheLimitIsRefused() { + SKIP_WITHOUT_SCRIPTING; // Without a rule the wire is drawn, as on master QCOMPARE(addWireToWiredTerminal(QFINDTESTDATA("fixtures/qet_bug_repro_resaved.qet"), false), QStringLiteral("true")); From 9a55939bb40e5fd1fc6750f19b1728bf7c4d9bf9 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Sun, 4 Oct 2026 01:13:09 +1300 Subject: [PATCH 5/6] Warn that the wires-per-terminal rules are experimental Review of #1272 (scorpio810, after arummler's note that the rules would need redesigning if wires and conductors become separate objects): a warning box, with the standard warning icon, at the top of the rules in Settings > General and in Project properties. It says the feature is experimental, counts wires as QElectroTech models them today, and that the rules, and the user's settings, could change if wires and conductors are separated. English for translators: "Experimental feature. These rules count wires as QElectroTech represents them today; they could change, and your settings need redoing, if wires and conductors become separate objects in a future version." Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015FPuYPS4T7QuEwjNu22rXD --- cmake/qet_compilation_vars.cmake | 2 + .../configpage/generalconfigurationpage.cpp | 2 + .../ui/configpage/generalconfigurationpage.ui | 7 +++ sources/ui/configpage/projectconfigpages.cpp | 2 + sources/ui/wiringruleswarning.cpp | 59 +++++++++++++++++++ sources/ui/wiringruleswarning.h | 29 +++++++++ 6 files changed, 101 insertions(+) create mode 100644 sources/ui/wiringruleswarning.cpp create mode 100644 sources/ui/wiringruleswarning.h diff --git a/cmake/qet_compilation_vars.cmake b/cmake/qet_compilation_vars.cmake index 9d6e2ef3d..98902040f 100644 --- a/cmake/qet_compilation_vars.cmake +++ b/cmake/qet_compilation_vars.cmake @@ -797,6 +797,8 @@ set(QET_SRC_FILES ${QET_DIR}/sources/ui/importelementtextpatterndialog.h ${QET_DIR}/sources/ui/jumptoelementdialog.cpp ${QET_DIR}/sources/ui/jumptoelementdialog.h + ${QET_DIR}/sources/ui/wiringruleswarning.cpp + ${QET_DIR}/sources/ui/wiringruleswarning.h ${QET_DIR}/sources/ui/inditextpropertieswidget.cpp ${QET_DIR}/sources/ui/inditextpropertieswidget.h ${QET_DIR}/sources/ui/linksingleelementwidget.cpp diff --git a/sources/ui/configpage/generalconfigurationpage.cpp b/sources/ui/configpage/generalconfigurationpage.cpp index aa6e63989..7d66a34b7 100644 --- a/sources/ui/configpage/generalconfigurationpage.cpp +++ b/sources/ui/configpage/generalconfigurationpage.cpp @@ -28,6 +28,7 @@ #include "../../qetmessagebox.h" #include "../../textgrid.h" #include "../../wiringrules.h" +#include "../wiringruleswarning.h" #include "../../editor/terminalnamecheck.h" #include "../../ElementsCollection/qetlabelsfile.h" #include "../prefixconfigurationdialog.h" @@ -117,6 +118,7 @@ GeneralConfigurationPage::GeneralConfigurationPage(QWidget *parent) : ui->m_wiring_rules_cb->setChecked(WiringRules::masterEnabled()); { //The rules every project follows unless it sets its own (#1158) + ui->m_wiring_rules_warning_layout->addWidget(WiringRulesWarning::create(this)); 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); diff --git a/sources/ui/configpage/generalconfigurationpage.ui b/sources/ui/configpage/generalconfigurationpage.ui index 427c9fc7b..6c91701b4 100644 --- a/sources/ui/configpage/generalconfigurationpage.ui +++ b/sources/ui/configpage/generalconfigurationpage.ui @@ -83,6 +83,13 @@ + + + + 20 + + + diff --git a/sources/ui/configpage/projectconfigpages.cpp b/sources/ui/configpage/projectconfigpages.cpp index 4531ffcea..43fd863b8 100644 --- a/sources/ui/configpage/projectconfigpages.cpp +++ b/sources/ui/configpage/projectconfigpages.cpp @@ -27,6 +27,7 @@ #include "../qeticons.h" #include "../qetproject.h" #include "../wiringrules.h" +#include "../wiringruleswarning.h" #include "../borderpropertieswidget.h" #include "../conductorpropertieswidget.h" #include "../diagramcontextwidget.h" @@ -306,6 +307,7 @@ void ProjectMainConfigPage::initLayout() main_layout0 -> addSpacing(10); QVBoxLayout *wiring_rules_layout = new QVBoxLayout(wiring_rules_gb_); + wiring_rules_layout -> addWidget(WiringRulesWarning::create(wiring_rules_gb_)); wiring_rules_layout -> addWidget(use_application_rules_cb_); QHBoxLayout *max_wires_layout = new QHBoxLayout(); max_wires_layout -> addWidget(max_wires_label_); diff --git a/sources/ui/wiringruleswarning.cpp b/sources/ui/wiringruleswarning.cpp new file mode 100644 index 000000000..2024ad633 --- /dev/null +++ b/sources/ui/wiringruleswarning.cpp @@ -0,0 +1,59 @@ +/* + 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 . +*/ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "wiringruleswarning.h" + +#include +#include +#include +#include +#include + +/** + @brief WiringRulesWarning::create + A warning box shown wherever the wires-per-terminal rules are set + (discussion #1158, review of #1272): the rules count wires on a terminal + as QElectroTech models them today, and would have to be redesigned if + wires and conductors become separate objects, so they are experimental. + @param parent + @return the box, a framed icon and text +*/ +QWidget *WiringRulesWarning::create(QWidget *parent) +{ + auto frame = new QFrame(parent); + frame->setFrameShape(QFrame::StyledPanel); + + auto icon = new QLabel(frame); + const int size = frame->style()->pixelMetric(QStyle::PM_SmallIconSize); + icon->setPixmap(frame->style()->standardIcon(QStyle::SP_MessageBoxWarning).pixmap(size, size)); + icon->setAlignment(Qt::AlignTop); + + auto text = new QLabel(QApplication::translate( + "WiringRulesWarning", + "Fonction expérimentale. Ces règles comptent les conducteurs tels " + "que QElectroTech les représente aujourd'hui ; elles pourraient " + "changer, et vos réglages devoir être refaits, si les fils et les " + "conducteurs deviennent des objets distincts dans une version future."), + frame); + text->setWordWrap(true); + + auto layout = new QHBoxLayout(frame); + layout->addWidget(icon); + layout->addWidget(text, 1); + return frame; +} diff --git a/sources/ui/wiringruleswarning.h b/sources/ui/wiringruleswarning.h new file mode 100644 index 000000000..4d7d690a1 --- /dev/null +++ b/sources/ui/wiringruleswarning.h @@ -0,0 +1,29 @@ +/* + 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 . +*/ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef WIRINGRULESWARNING_H +#define WIRINGRULESWARNING_H + +class QWidget; + +namespace WiringRulesWarning +{ + QWidget *create(QWidget *parent = nullptr); +} + +#endif // WIRINGRULESWARNING_H From 3150b420871dc3fcadeb8ec0ccc6caf30b44b5f9 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Sun, 4 Oct 2026 01:43:21 +1300 Subject: [PATCH 6/6] Warn with a pop-up, not a box, when a wires-per-terminal rule is turned on Review of #1272: scorpio810 asked for a warning box for users; the inline box added in 9a55939bb is replaced by a pop-up, as the element editor gives for unnamed terminals. When OK in Settings > General or in Project properties turns a rule on (a limit where there was none, or one wire per folio report), a warning says the rules are experimental, may change with settings to redo if wires and conductors become separate objects, and can all be turned off in Settings > General. OK goes on. Changing a limit that was already on, or turning rules off, shows nothing. tst_wiringrules: when the warning is due (WiringRules::turnsRuleOn()). Checked in the GUI from both pages, and that a plain OK shows nothing. English for translators: "Warning"; "The wires-per-terminal rules are an experimental feature. They count wires as QElectroTech represents them today. They could change, and your settings need redoing, if wires and conductors become separate objects in a future version. All these rules can be turned off in Configure QElectroTech > General." Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015FPuYPS4T7QuEwjNu22rXD --- .../configpage/generalconfigurationpage.cpp | 7 ++- .../ui/configpage/generalconfigurationpage.ui | 7 --- sources/ui/configpage/projectconfigpages.cpp | 6 ++- sources/ui/wiringruleswarning.cpp | 50 +++++++------------ sources/ui/wiringruleswarning.h | 2 +- sources/wiringrules.cpp | 12 +++++ sources/wiringrules.h | 1 + tests/qttest/tst_wiringrules.cpp | 17 +++++++ 8 files changed, 60 insertions(+), 42 deletions(-) diff --git a/sources/ui/configpage/generalconfigurationpage.cpp b/sources/ui/configpage/generalconfigurationpage.cpp index 7d66a34b7..826679667 100644 --- a/sources/ui/configpage/generalconfigurationpage.cpp +++ b/sources/ui/configpage/generalconfigurationpage.cpp @@ -118,7 +118,6 @@ GeneralConfigurationPage::GeneralConfigurationPage(QWidget *parent) : ui->m_wiring_rules_cb->setChecked(WiringRules::masterEnabled()); { //The rules every project follows unless it sets its own (#1158) - ui->m_wiring_rules_warning_layout->addWidget(WiringRulesWarning::create(this)); 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); @@ -353,10 +352,14 @@ void GeneralConfigurationPage::applyConf() 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(); + const WiringRules::Settings before = WiringRules::applicationSettings(); + WiringRules::Settings rules = before; 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); + if (WiringRules::masterEnabled() && WiringRules::turnsRuleOn(before, rules)) { + WiringRulesWarning::show(this); + } } settings.setValue("diagrameditor/autosave-interval", ui->m_autosave_sb->value()); diff --git a/sources/ui/configpage/generalconfigurationpage.ui b/sources/ui/configpage/generalconfigurationpage.ui index 6c91701b4..427c9fc7b 100644 --- a/sources/ui/configpage/generalconfigurationpage.ui +++ b/sources/ui/configpage/generalconfigurationpage.ui @@ -83,13 +83,6 @@ - - - - 20 - - - diff --git a/sources/ui/configpage/projectconfigpages.cpp b/sources/ui/configpage/projectconfigpages.cpp index 43fd863b8..fdb9444fb 100644 --- a/sources/ui/configpage/projectconfigpages.cpp +++ b/sources/ui/configpage/projectconfigpages.cpp @@ -186,8 +186,13 @@ void ProjectMainConfigPage::applyProjectConf() wiring_rules.one_wire_per_report = one_wire_per_report_cb_ -> isChecked(); } if (m_project -> projectWiringRules() != wiring_rules) { + const WiringRules::Settings before = m_project -> wiringRules(); m_project -> setWiringRules(wiring_rules); modified_project = true; + if (WiringRules::masterEnabled() + && WiringRules::turnsRuleOn(before, m_project -> wiringRules())) { + WiringRulesWarning::show(this); + } } if (modified_project) { @@ -307,7 +312,6 @@ void ProjectMainConfigPage::initLayout() main_layout0 -> addSpacing(10); QVBoxLayout *wiring_rules_layout = new QVBoxLayout(wiring_rules_gb_); - wiring_rules_layout -> addWidget(WiringRulesWarning::create(wiring_rules_gb_)); wiring_rules_layout -> addWidget(use_application_rules_cb_); QHBoxLayout *max_wires_layout = new QHBoxLayout(); max_wires_layout -> addWidget(max_wires_label_); diff --git a/sources/ui/wiringruleswarning.cpp b/sources/ui/wiringruleswarning.cpp index 2024ad633..2bd975e6f 100644 --- a/sources/ui/wiringruleswarning.cpp +++ b/sources/ui/wiringruleswarning.cpp @@ -19,41 +19,29 @@ #include "wiringruleswarning.h" #include -#include -#include -#include -#include +#include /** - @brief WiringRulesWarning::create - A warning box shown wherever the wires-per-terminal rules are set - (discussion #1158, review of #1272): the rules count wires on a terminal - as QElectroTech models them today, and would have to be redesigned if - wires and conductors become separate objects, so they are experimental. + @brief WiringRulesWarning::show + Warn, when a wires-per-terminal rule is turned on (discussion #1158, + review of #1272), that the rules are experimental: they count wires on + a terminal as QElectroTech models them today, and would have to be + redesigned if wires and conductors become separate objects. A plain + warning, as the element editor gives for unnamed terminals: OK goes on. @param parent - @return the box, a framed icon and text */ -QWidget *WiringRulesWarning::create(QWidget *parent) +void WiringRulesWarning::show(QWidget *parent) { - auto frame = new QFrame(parent); - frame->setFrameShape(QFrame::StyledPanel); - - auto icon = new QLabel(frame); - const int size = frame->style()->pixelMetric(QStyle::PM_SmallIconSize); - icon->setPixmap(frame->style()->standardIcon(QStyle::SP_MessageBoxWarning).pixmap(size, size)); - icon->setAlignment(Qt::AlignTop); - - auto text = new QLabel(QApplication::translate( + QMessageBox::warning( + parent, + QApplication::translate("WiringRulesWarning", "Avertissement"), + QApplication::translate( "WiringRulesWarning", - "Fonction expérimentale. Ces règles comptent les conducteurs tels " - "que QElectroTech les représente aujourd'hui ; elles pourraient " - "changer, et vos réglages devoir être refaits, si les fils et les " - "conducteurs deviennent des objets distincts dans une version future."), - frame); - text->setWordWrap(true); - - auto layout = new QHBoxLayout(frame); - layout->addWidget(icon); - layout->addWidget(text, 1); - return frame; + "Les règles de conducteurs par borne sont une fonction expérimentale." + "

Elles comptent les conducteurs tels que QElectroTech les représente " + "aujourd'hui. Elles pourraient changer, et vos réglages devoir être refaits, " + "si les fils et les conducteurs deviennent des objets distincts dans une " + "version future." + "

Toutes ces règles peuvent être désactivées dans Configurer " + "QElectroTech > Général.")); } diff --git a/sources/ui/wiringruleswarning.h b/sources/ui/wiringruleswarning.h index 4d7d690a1..033a2d6f6 100644 --- a/sources/ui/wiringruleswarning.h +++ b/sources/ui/wiringruleswarning.h @@ -23,7 +23,7 @@ class QWidget; namespace WiringRulesWarning { - QWidget *create(QWidget *parent = nullptr); + void show(QWidget *parent); } #endif // WIRINGRULESWARNING_H diff --git a/sources/wiringrules.cpp b/sources/wiringrules.cpp index d0d4e5fc8..19d7380e4 100644 --- a/sources/wiringrules.cpp +++ b/sources/wiringrules.cpp @@ -173,3 +173,15 @@ bool WiringRules::hasRoom(int limit, int wires) { return limit <= 0 || wires < limit; } + +/** + @brief WiringRules::turnsRuleOn + @param before, after : the rules in force before and after a change + @return true if the change turns on a rule that was off: QElectroTech + then warns that the rules are experimental (review of #1272). +*/ +bool WiringRules::turnsRuleOn(const Settings &before, const Settings &after) +{ + return (after.max_wires > 0 && before.max_wires <= 0) + || (after.one_wire_per_report && !before.one_wire_per_report); +} diff --git a/sources/wiringrules.h b/sources/wiringrules.h index af6311886..c4eef1db8 100644 --- a/sources/wiringrules.h +++ b/sources/wiringrules.h @@ -72,6 +72,7 @@ namespace WiringRules Settings applicationSettings(); void setApplicationSettings(const Settings &settings); Settings effective(const Settings &project, const Settings &application); + bool turnsRuleOn(const Settings &before, const Settings &after); 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 f7f258d68..be4d283e2 100644 --- a/tests/qttest/tst_wiringrules.cpp +++ b/tests/qttest/tst_wiringrules.cpp @@ -195,6 +195,23 @@ private slots: QCOMPARE(WiringRules::effective(own, application).max_wires, 6); } + void warnsWhenARuleIsTurnedOn() + { + WiringRules::Settings off; + WiringRules::Settings limit; + limit.max_wires = 4; + WiringRules::Settings report; + report.one_wire_per_report = true; + + QVERIFY(WiringRules::turnsRuleOn(off, limit)); + QVERIFY(WiringRules::turnsRuleOn(off, report)); + QVERIFY(WiringRules::turnsRuleOn(limit, [&]{ auto s = limit; s.one_wire_per_report = true; return s; }())); + // Changing a limit that was already on, or turning rules off: no warning + QVERIFY(!WiringRules::turnsRuleOn(limit, [&]{ auto s = limit; s.max_wires = 2; return s; }())); + QVERIFY(!WiringRules::turnsRuleOn(limit, off)); + QVERIFY(!WiringRules::turnsRuleOn(off, off)); + } + void xmlRoundTrip() { QDomDocument doc;