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"