mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-05 02:24:13 +02:00
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 <wiring_rules .../> 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015FPuYPS4T7QuEwjNu22rXD
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 &);
|
||||
|
||||
@@ -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 <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 +2265,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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -70,6 +70,19 @@
|
||||
</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>
|
||||
<widget class="Line" name="line_2">
|
||||
<property name="orientation">
|
||||
@@ -1297,6 +1310,7 @@ 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_use_windows_mode_rb</tabstop>
|
||||
<tabstop>m_use_tab_mode_rb</tabstop>
|
||||
<tabstop>m_save_label_paste</tabstop>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
//######################################################################################//
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 <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;
|
||||
}
|
||||
|
||||
/**
|
||||
@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 <wiring_rules> 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 <wiring_rules> 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;
|
||||
}
|
||||
@@ -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 <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.
|
||||
|
||||
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
|
||||
@@ -460,6 +460,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(
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
// 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"
|
||||
|
||||
// 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.
|
||||
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 <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)
|
||||
{
|
||||
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));
|
||||
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 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("<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");
|
||||
}
|
||||
|
||||
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("<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"));
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_GUILESS_MAIN(tst_wiringrules)
|
||||
|
||||
#include "tst_wiringrules.moc"
|
||||
Reference in New Issue
Block a user