Merge pull request #1272 from ispyisail/feature/wires-per-terminal

Add an optional limit on wires per terminal (discussion #1158)
This commit is contained in:
Laurent Trinques
2026-10-03 16:35:10 +02:00
committed by GitHub
17 changed files with 992 additions and 3 deletions
+4
View File
@@ -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
@@ -797,6 +799,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
+5 -2
View File
@@ -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 <QPair <Terminal *, Terminal *> > Element::AlignedFreeTerminals() const
{
@@ -1357,7 +1358,9 @@ QList <QPair <Terminal *, Terminal *> > Element::AlignedFreeTerminals() const
{
Terminal *other_terminal =
terminal -> alignedWithTerminal();
if (other_terminal)
if (other_terminal
&& terminal->hasRoomForWire()
&& other_terminal->hasRoomForWire())
list << qMakePair(terminal, other_terminal);
}
}
+51 -1
View File
@@ -26,6 +26,9 @@
#include "../qetgraphicsitem/conductor.h"
#include "../qetgraphicsitem/element.h"
#include "conductortextitem.h"
#include "../wiringrules.h"
#include <QToolTip>
#include <QtCore/qnumeric.h>
#include <utility>
@@ -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
+2
View File
@@ -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 &);
+54
View File
@@ -1257,6 +1257,37 @@ void QETProject::setWireHops(WireHops::Mode mode)
}
}
/**
@brief QETProject::wiringRules
@return how many wires a terminal of this project may take
(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 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)
{
m_wiring_rules = rules;
}
/**
@brief QETProject::autoBreakConductor
@return true if use of auto break conductor is authorized.
@@ -1367,6 +1398,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 +1909,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 +2103,17 @@ void QETProject::readWireHopsXml(QDomDocument &xml_project)
: WireHops::fromString(crossings.attribute(QStringLiteral("hop")));
}
/**
@brief QETProject::readWiringRulesXml
Read the <wiring_rules> 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 +2276,16 @@ void QETProject::writeWireHopsXml(QDomElement &xml_element)
xml_element.appendChild(crossings);
}
/**
@brief QETProject::writeWiringRulesXml
Export the project's wiring rules as a <wiring_rules> 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
+7
View File
@@ -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,9 @@ class QETProject : public QObject
void setAutoConductor (bool ac);
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);
void autoFolioNumberingNewFolios ();
@@ -308,11 +312,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 +377,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;
+5
View File
@@ -973,6 +973,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;
@@ -27,6 +27,8 @@
#include "../../utils/qetutils.h"
#include "../../qetmessagebox.h"
#include "../../textgrid.h"
#include "../../wiringrules.h"
#include "../wiringruleswarning.h"
#include "../../editor/terminalnamecheck.h"
#include "../../ElementsCollection/qetlabelsfile.h"
#include "../prefixconfigurationdialog.h"
@@ -113,6 +115,20 @@ 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());
{
//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());
@@ -334,6 +350,17 @@ 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());
{
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());
settings.setValue("diagrameditor/grid_display_startup", ui->grid_startup_cb->isChecked());
@@ -70,6 +70,76 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="m_wiring_rules_cb">
<property name="toolTip">
<string>Décoché, aucune règle de conducteurs par borne n'est appliquée, quels que soient les réglages des projets.</string>
</property>
<property name="text">
<string>Appliquer les règles de conducteurs par borne des projets</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="m_wiring_max_wires_layout">
<property name="leftMargin">
<number>20</number>
</property>
<item>
<widget class="QLabel" name="m_wiring_max_wires_label">
<property name="text">
<string>Nombre maximal de conducteurs par borne :</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="m_wiring_max_wires_sb">
<property name="minimumSize">
<size>
<width>120</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>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.</string>
</property>
<property name="specialValueText">
<string>Sans limite</string>
</property>
<property name="maximum">
<number>99</number>
</property>
</widget>
</item>
<item>
<spacer name="m_wiring_max_wires_spacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="m_wiring_report_layout">
<property name="leftMargin">
<number>20</number>
</property>
<item>
<widget class="QCheckBox" name="m_wiring_one_wire_per_report_cb">
<property name="toolTip">
<string>Un renvoi de folio est un point virtuel : il ne reçoit qu'un conducteur, celui qui continue sur l'autre folio.</string>
</property>
<property name="text">
<string>Un seul conducteur par renvoi de folio</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="Line" name="line_2">
<property name="orientation">
@@ -1297,6 +1367,9 @@ Vous pouvez spécifier ici la valeur par défaut de ce champ pour les éléments
<tabstop>m_use_gesture_trackpad</tabstop>
<tabstop>m_zoom_out_beyond_folio</tabstop>
<tabstop>m_conductor_properties_panel</tabstop>
<tabstop>m_wiring_rules_cb</tabstop>
<tabstop>m_wiring_max_wires_sb</tabstop>
<tabstop>m_wiring_one_wire_per_report_cb</tabstop>
<tabstop>m_use_windows_mode_rb</tabstop>
<tabstop>m_use_tab_mode_rb</tabstop>
<tabstop>m_save_label_paste</tabstop>
@@ -26,6 +26,8 @@
#include "../qet.h"
#include "../qeticons.h"
#include "../qetproject.h"
#include "../wiringrules.h"
#include "../wiringruleswarning.h"
#include "../borderpropertieswidget.h"
#include "../conductorpropertieswidget.h"
#include "../diagramcontextwidget.h"
@@ -176,6 +178,23 @@ void ProjectMainConfigPage::applyProjectConf()
modified_project = true;
}
//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) {
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) {
m_project -> setModified(true);
}
@@ -225,6 +244,39 @@ 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"));
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);
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 +309,18 @@ 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_);
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_);
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 +344,34 @@ 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);
use_application_rules_cb_ -> setEnabled(master);
max_wires_label_ -> setEnabled(own);
max_wires_sb_ -> setEnabled(own);
one_wire_per_report_cb_ -> setEnabled(own);
}
/**
@@ -307,6 +399,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);
}
//######################################################################################//
@@ -24,6 +24,8 @@ class QComboBox;
class QLineEdit;
class QCheckBox;
class QPushButton;
class QSpinBox;
class QGroupBox;
class QETProject;
class BorderPropertiesWidget;
class ConductorPropertiesWidget;
@@ -114,6 +116,7 @@ class ProjectMainConfigPage : public ProjectConfigPage {
private slots:
void resetUsageTracker();
void updateWiringRulesWidgets();
// attributes
protected:
@@ -128,6 +131,12 @@ class ProjectMainConfigPage : public ProjectConfigPage {
QPushButton *usage_reset_pb_;
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_;
QLabel *wiring_rules_off_label_;
};
class ProjectAutoNumConfigPage : public ProjectConfigPage {
+47
View File
@@ -0,0 +1,47 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
// SPDX-License-Identifier: GPL-2.0-or-later
#include "wiringruleswarning.h"
#include <QApplication>
#include <QMessageBox>
/**
@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
*/
void WiringRulesWarning::show(QWidget *parent)
{
QMessageBox::warning(
parent,
QApplication::translate("WiringRulesWarning", "Avertissement"),
QApplication::translate(
"WiringRulesWarning",
"<b>Les règles de conducteurs par borne sont une fonction expérimentale.</b>"
"<br><br>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."
"<br><br>Toutes ces règles peuvent être désactivées dans Configurer "
"QElectroTech > Général."));
}
+29
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
// SPDX-License-Identifier: GPL-2.0-or-later
#ifndef WIRINGRULESWARNING_H
#define WIRINGRULESWARNING_H
class QWidget;
namespace WiringRulesWarning
{
void show(QWidget *parent);
}
#endif // WIRINGRULESWARNING_H
+187
View File
@@ -0,0 +1,187 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
// SPDX-License-Identifier: GPL-2.0-or-later
#include "wiringrules.h"
#include <QDomDocument>
#include <QDomElement>
#include <QSettings>
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;
//Same for the application's rules
bool application_cached = false;
WiringRules::Settings application_cache;
}
/**
@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::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 <wiring_rules> child of \a project_root,
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)
{
Settings settings;
const QDomElement rules = project_root.firstChildElement(element_name);
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;
}
/**
@brief WiringRules::toXml
Write \a settings as a <wiring_rules> 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.own) {
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;
}
/**
@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);
}
+84
View File
@@ -0,0 +1,84 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
// 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.
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.
*/
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";
///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
bool one_wire_per_report = false;
bool isDefault() const { return *this == Settings(); }
bool operator==(const Settings &other) const {
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); }
};
bool masterEnabled();
void setMasterEnabled(bool enabled);
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);
int limit(const Settings &settings, bool master_enabled, bool is_report);
bool hasRoom(int limit, int wires);
}
#endif // WIRINGRULES_H
+15
View File
@@ -468,6 +468,21 @@ target_link_libraries(tst_wirehops PRIVATE Qt::Test Qt::Gui)
target_compile_definitions(tst_wirehops PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
# 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=\"$<TARGET_FILE:qelectrotech>\"")
# The routing behind qet.routeConductor(): conductorrouter.cpp needs only
# Qt Core, so the geometry is tested without a scene.
add_executable(
+300
View File
@@ -0,0 +1,300 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QtTest>
#include <QDir>
#include <QDomDocument>
#include <QFile>
#include <QProcess>
#include <QProcessEnvironment>
#include <QTemporaryDir>
#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 <wiring_rules> 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, @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));
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 || application_max_wires) {
QFile ini(settings + QStringLiteral("/QElectroTech/QElectroTech.ini"));
if (ini.open(QIODevice::WriteOnly | QIODevice::Text))
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"));
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 <newdiagrams>, 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("<newdiagrams"));
if (newdiagrams < 0)
return {};
text.insert(newdiagrams, rules + QStringLiteral("\n "));
const QString path = m_dir.filePath(QStringLiteral("fixture%1.qet").arg(m_run));
QFile out(path);
if (!out.open(QIODevice::WriteOnly | QIODevice::Text))
return {};
out.write(text.toUtf8());
return path;
}
// Runs --resave on @p in, returns the saved file's text (empty on failure)
QString resave(const QString &in)
{
const QString out = m_dir.filePath(QStringLiteral("out%1.qet").arg(m_run));
QProcess proc;
proc.setProcessEnvironment(sandbox());
proc.start(QStringLiteral(QET_TEST_BINARY_PATH), {QStringLiteral("--resave"), in, out});
if (!proc.waitForFinished(120000) || proc.exitCode() != 0)
return {};
QFile file(out);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
return {};
return QString::fromUtf8(file.readAll());
}
// 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,
int application_max_wires = 0)
{
const QString script_path = m_dir.filePath(QStringLiteral("probe%1.js").arg(m_run));
QFile script(script_path);
if (!script.open(QIODevice::WriteOnly))
return {};
// The first end of the first conductor is a terminal with a wire.
// Any terminal of another element with no wire is the other end.
script.write(
"var end = qet.conductorEnds(0, qet.conductorUuids(0)[0])[0].split(' terminal ');\n"
"var wired = {};\n"
"qet.conductorUuids(0).forEach(function (u) {\n"
" qet.conductorEnds(0, u).forEach(function (e) { wired[e] = true; }); });\n"
"var free = null;\n"
"qet.elementUuids(0).forEach(function (el) {\n"
" if (free || el == end[0]) return;\n"
" var n = qet.elementTerminals(0, el).length;\n"
" for (var i = 0; i < n && !free; ++i)\n"
" if (!wired[el + ' terminal ' + i]) free = [el, i]; });\n"
"qet.log('PROBE ' + (free ? qet.addConductor(0, end[0], parseInt(end[1]), free[0], free[1])\n"
" : 'nofree'));\n");
script.close();
QProcess proc;
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 {};
const QString out = QString::fromUtf8(proc.readAllStandardOutput()
+ proc.readAllStandardError());
const QString mark = QStringLiteral("PROBE ");
for (const QString &line : out.split(QLatin1Char('\n'))) {
const int i = line.indexOf(mark);
if (i >= 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 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 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;
QDomElement root = doc.createElement(QStringLiteral("project"));
doc.appendChild(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);
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("<wiring_rules max_wires_per_terminal=\"2\" one_wire_per_report=\"true\"/>"));
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");
// A project's own "no rule" is kept too: it overrides the application's
const QString own_none = fixtureWith(QStringLiteral("<wiring_rules/>"));
QVERIFY(!own_none.isEmpty());
const QString saved_none = resave(own_none);
QVERIFY2(!saved_none.isEmpty(), "--resave failed");
QVERIFY2(saved_none.contains(QLatin1String("<wiring_rules/>")),
"a project's own rules were lost on save");
}
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"));
// One wire per terminal: the terminal is full
const QString limited = fixtureWith(
QStringLiteral("<wiring_rules max_wires_per_terminal=\"1\"/>"));
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"));
// 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("<wiring_rules/>"));
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"));
}
};
QTEST_GUILESS_MAIN(tst_wiringrules)
#include "tst_wiringrules.moc"