Set the wires-per-terminal limits for the application, a project can override

The limits were per project only. They are now set once in Settings >
General, under the master switch: the most wires per terminal and "one
wire per folio report". Every project follows them.

Project properties > "Conducteurs par borne" gets "Utiliser les réglages
de l'application", ticked by default, showing the application's values
greyed. Unticked, the project keeps its own values, saved in the project
as before, so a drawing that needs its own limit carries it to any
computer. A project's own "no limit" overrides an application limit too.

QETProject::wiringRules() now returns the rules in force (the project's
own, or the application's), so the code that checks a wire is unchanged;
projectWiringRules() returns what the project itself stores. A project
that follows the application still writes nothing, so it saves exactly as
before.

tst_wiringrules: which rules win, the save round trip of a project's own
"no rule", and through the real binary that the application's limit
refuses a wire unless the project overrides it or the master switch is
off (checked to fail with the application's rules ignored). ctest 55/55.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015FPuYPS4T7QuEwjNu22rXD
This commit is contained in:
ispyisail
2026-10-03 23:16:43 +13:00
parent 20e134f2bd
commit 7ba820f8f0
9 changed files with 268 additions and 26 deletions
+14 -3
View File
@@ -1260,16 +1260,27 @@ void QETProject::setWireHops(WireHops::Mode mode)
/**
@brief QETProject::wiringRules
@return how many wires a terminal of this project may take
(discussion #1158); the default when the project sets no rule.
(discussion #1158): the project's own rules when it sets them,
otherwise the application's (Settings > General).
*/
WiringRules::Settings QETProject::wiringRules() const {
return WiringRules::effective(m_wiring_rules, WiringRules::applicationSettings());
}
/**
@brief QETProject::projectWiringRules
@return the rules as the project stores them: Settings::own false when
it follows the application's.
*/
WiringRules::Settings QETProject::projectWiringRules() const {
return m_wiring_rules;
}
/**
@brief QETProject::setWiringRules
Set how many wires a terminal of this project may take. Only wires
drawn from now on are affected: none already drawn is removed.
Set the project's own wiring rules, or (Settings::own false) make it
follow the application's. Only wires drawn from now on are affected:
none already drawn is removed.
@param rules
*/
void QETProject::setWiringRules(const WiringRules::Settings &rules)
+1
View File
@@ -213,6 +213,7 @@ class QETProject : public QObject
WireHops::Mode wireHops() const;
void setWireHops(WireHops::Mode mode);
WiringRules::Settings wiringRules() const;
WiringRules::Settings projectWiringRules() const;
void setWiringRules(const WiringRules::Settings &rules);
void setAutoBreakConductor (bool abc);
void setAutoElement (bool ae);
@@ -115,6 +115,19 @@ GeneralConfigurationPage::GeneralConfigurationPage(QWidget *parent) :
ui->m_zoom_out_beyond_folio->setChecked(settings.value("diagrameditor/zoom-out-beyond-of-folio", false).toBool());
ui->m_conductor_properties_panel->setChecked(settings.value("diagrameditor/conductor_properties_panel", false).toBool());
ui->m_wiring_rules_cb->setChecked(WiringRules::masterEnabled());
{
//The rules every project follows unless it sets its own (#1158)
const WiringRules::Settings rules = WiringRules::applicationSettings();
ui->m_wiring_max_wires_sb->setValue(rules.max_wires);
ui->m_wiring_one_wire_per_report_cb->setChecked(rules.one_wire_per_report);
auto enable = [this](bool on) {
ui->m_wiring_max_wires_label->setEnabled(on);
ui->m_wiring_max_wires_sb->setEnabled(on);
ui->m_wiring_one_wire_per_report_cb->setEnabled(on);
};
enable(ui->m_wiring_rules_cb->isChecked());
connect(ui->m_wiring_rules_cb, &QCheckBox::toggled, this, enable);
}
ui->m_use_gesture_trackpad->setChecked(settings.value("diagramview/gestures", false).toBool());
ui->m_save_label_paste->setChecked(settings.value("diagramcommands/erase-label-on-copy", true).toBool());
ui->m_enable_scripting->setChecked(QetSettings::scriptingEnabled());
@@ -337,6 +350,12 @@ void GeneralConfigurationPage::applyConf()
settings.setValue("diagrameditor/zoom-out-beyond-of-folio", ui->m_zoom_out_beyond_folio->isChecked());
settings.setValue("diagrameditor/conductor_properties_panel", ui->m_conductor_properties_panel->isChecked());
WiringRules::setMasterEnabled(ui->m_wiring_rules_cb->isChecked());
{
WiringRules::Settings rules = WiringRules::applicationSettings();
rules.max_wires = ui->m_wiring_max_wires_sb->value();
rules.one_wire_per_report = ui->m_wiring_one_wire_per_report_cb->isChecked();
WiringRules::setApplicationSettings(rules);
}
settings.setValue("diagrameditor/autosave-interval", ui->m_autosave_sb->value());
settings.setValue("diagrameditor/grid_display_startup", ui->grid_startup_cb->isChecked());
@@ -83,6 +83,57 @@
</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="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">
@@ -1311,6 +1362,8 @@ Vous pouvez spécifier ici la valeur par défaut de ce champ pour les éléments
<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>
+43 -7
View File
@@ -177,10 +177,14 @@ void ProjectMainConfigPage::applyProjectConf()
modified_project = true;
}
WiringRules::Settings wiring_rules = m_project -> wiringRules();
wiring_rules.max_wires = max_wires_sb_ -> value();
wiring_rules.one_wire_per_report = one_wire_per_report_cb_ -> isChecked();
if (m_project -> wiringRules() != wiring_rules) {
//Following the application's rules stores nothing in the project
WiringRules::Settings wiring_rules;
if (!use_application_rules_cb_ -> isChecked()) {
wiring_rules.own = true;
wiring_rules.max_wires = max_wires_sb_ -> value();
wiring_rules.one_wire_per_report = one_wire_per_report_cb_ -> isChecked();
}
if (m_project -> projectWiringRules() != wiring_rules) {
m_project -> setWiringRules(wiring_rules);
modified_project = true;
}
@@ -237,6 +241,20 @@ void ProjectMainConfigPage::initWidgets()
//How many wires a terminal may take (discussion #1158)
wiring_rules_gb_ = new QGroupBox(tr("Conducteurs par borne", "group box title"));
use_application_rules_cb_ = new QCheckBox(tr("Utiliser les réglages de l'application", "checkbox label"));
use_application_rules_cb_ -> setToolTip(tr("Les réglages de Configurer QElectroTech > Général s'appliquent. "
"Décochez pour donner à ce projet ses propres réglages, enregistrés dans le projet.",
"tooltip"));
connect(use_application_rules_cb_, &QCheckBox::toggled, this, [this](bool use) {
//Show the values that will apply: the application's, or the
//application's as a starting point for the project's own
if (use) {
const WiringRules::Settings application = WiringRules::applicationSettings();
max_wires_sb_ -> setValue(application.max_wires);
one_wire_per_report_cb_ -> setChecked(application.one_wire_per_report);
}
updateWiringRulesWidgets();
});
max_wires_label_ = new QLabel(tr("Nombre maximal de conducteurs par borne :", "label when configuring"));
max_wires_sb_ = new QSpinBox();
max_wires_sb_ -> setRange(0, 99);
@@ -288,6 +306,7 @@ void ProjectMainConfigPage::initLayout()
main_layout0 -> addSpacing(10);
QVBoxLayout *wiring_rules_layout = new QVBoxLayout(wiring_rules_gb_);
wiring_rules_layout -> addWidget(use_application_rules_cb_);
QHBoxLayout *max_wires_layout = new QHBoxLayout();
max_wires_layout -> addWidget(max_wires_label_);
max_wires_layout -> addWidget(max_wires_sb_);
@@ -320,16 +339,33 @@ void ProjectMainConfigPage::readValuesFromProject()
const int wire_hops_index = wire_hops_cb_ -> findData(WireHops::toString(m_project -> wireHops()));
wire_hops_cb_ -> setCurrentIndex(qMax(0, wire_hops_index));
//The rules that apply: the project's own or the application's
const WiringRules::Settings wiring_rules = m_project -> wiringRules();
{
const QSignalBlocker blocker(use_application_rules_cb_);
use_application_rules_cb_ -> setChecked(!wiring_rules.own);
}
max_wires_sb_ -> setValue(wiring_rules.max_wires);
one_wire_per_report_cb_ -> setChecked(wiring_rules.one_wire_per_report);
updateWiringRulesWidgets();
}
/**
@brief ProjectMainConfigPage::updateWiringRulesWidgets
Enable the wiring rule fields only when they can change something: the
master switch is on and the project sets its own rules.
*/
void ProjectMainConfigPage::updateWiringRulesWidgets()
{
//The master switch wins over every project: say so rather than
//let the user set a rule that does nothing.
const bool master = WiringRules::masterEnabled();
const bool own = master && !use_application_rules_cb_ -> isChecked();
wiring_rules_off_label_ -> setVisible(!master);
max_wires_label_ -> setEnabled(master);
max_wires_sb_ -> setEnabled(master);
one_wire_per_report_cb_ -> setEnabled(master);
use_application_rules_cb_ -> setEnabled(master);
max_wires_label_ -> setEnabled(own);
max_wires_sb_ -> setEnabled(own);
one_wire_per_report_cb_ -> setEnabled(own);
}
/**
@@ -116,6 +116,7 @@ class ProjectMainConfigPage : public ProjectConfigPage {
private slots:
void resetUsageTracker();
void updateWiringRulesWidgets();
// attributes
protected:
@@ -131,6 +132,7 @@ class ProjectMainConfigPage : public ProjectConfigPage {
QLabel *wire_hops_label_;
QComboBox *wire_hops_cb_;
QGroupBox *wiring_rules_gb_;
QCheckBox *use_application_rules_cb_;
QLabel *max_wires_label_;
QSpinBox *max_wires_sb_;
QCheckBox *one_wire_per_report_cb_;
+54 -5
View File
@@ -30,6 +30,9 @@ namespace {
//The master switch is read on every wire drawn, so it is read from
//the settings once and kept; setMasterEnabled() keeps both in step.
int master_cache = -1;
//Same for the application's rules
bool application_cached = false;
WiringRules::Settings application_cache;
}
/**
@@ -57,11 +60,56 @@ void WiringRules::setMasterEnabled(bool enabled)
master_cache = enabled ? 1 : 0;
}
/**
@brief WiringRules::applicationSettings
@return the rules set for every project in Settings > General; a
project that sets its own (Settings::own) uses those instead.
*/
WiringRules::Settings WiringRules::applicationSettings()
{
if (!application_cached) {
const QSettings settings;
application_cache = Settings();
application_cache.max_wires = qMax(0, settings.value(max_wires_key, 0).toInt());
application_cache.one_wire_per_report = settings.value(one_wire_per_report_key, false).toBool();
application_cached = true;
}
return application_cache;
}
/**
@brief WiringRules::setApplicationSettings
Save the rules set for every project.
@param settings
*/
void WiringRules::setApplicationSettings(const Settings &settings)
{
QSettings qsettings;
qsettings.setValue(max_wires_key, qMax(0, settings.max_wires));
qsettings.setValue(one_wire_per_report_key, settings.one_wire_per_report);
application_cache = settings;
application_cache.own = false;
application_cached = true;
}
/**
@brief WiringRules::effective
@return the rules that apply to a project: its own when it sets them,
otherwise the application's.
*/
WiringRules::Settings WiringRules::effective(const Settings &project, const Settings &application)
{
Settings result = project.own ? project : application;
result.own = project.own;
return result;
}
/**
@brief WiringRules::fromXml
@param project_root : the root element of a project
@return the rules stored in the <wiring_rules> child of \a project_root,
the default (no rule) when there is none.
which the project then uses instead of the application's (Settings::own);
the default, which follows the application, when there is none.
*/
WiringRules::Settings WiringRules::fromXml(const QDomElement &project_root)
{
@@ -70,6 +118,7 @@ WiringRules::Settings WiringRules::fromXml(const QDomElement &project_root)
if (rules.isNull()) {
return settings;
}
settings.own = true;
settings.max_wires = qMax(0, rules.attribute(max_wires_attribute, QStringLiteral("0")).toInt());
settings.one_wire_per_report = rules.attribute(report_attribute) == QLatin1String("true");
return settings;
@@ -77,13 +126,13 @@ WiringRules::Settings WiringRules::fromXml(const QDomElement &project_root)
/**
@brief WiringRules::toXml
Write \a settings as a <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.
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.isDefault()) {
if (!settings.own) {
return;
}
QDomElement rules = project_root.ownerDocument().createElement(element_name);
+18 -3
View File
@@ -29,7 +29,10 @@ class QDomElement;
limit, a new wire that would go past it is refused. Wires already in
the drawing are never touched.
Every rule is off unless the project turns it on, and the application
The rules are set once for the application (Settings > General,
applicationSettings()) and apply to every project; a project can set
its own instead (Settings::own), saved in the project so it travels
with the drawing. Every rule is off by default, and the application
has a master switch (masterEnabled()) that turns all of them off in
every project at once. Kept free of any QGraphicsItem so the rules can
be tested on their own.
@@ -38,10 +41,17 @@ namespace WiringRules
{
///The QSettings key of the master switch
constexpr const char *settings_key = "diagrameditor/wiring_rules_enabled";
///The QSettings keys of the application's rules
constexpr const char *max_wires_key = "diagrameditor/wiring_rules_max_wires";
constexpr const char *one_wire_per_report_key = "diagrameditor/wiring_rules_one_wire_per_report";
///The rules a project chose. The default is "no rule".
///A set of rules: the application's, or a project's. The default is
///"no rule".
struct Settings
{
///For a project: it uses these values rather than the
///application's. Ignored in the application's own settings.
bool own = false;
///Most wires a terminal may take, 0 for no limit
int max_wires = 0;
///A folio report takes one wire only
@@ -49,7 +59,8 @@ namespace WiringRules
bool isDefault() const { return *this == Settings(); }
bool operator==(const Settings &other) const {
return max_wires == other.max_wires
return own == other.own
&& max_wires == other.max_wires
&& one_wire_per_report == other.one_wire_per_report;
}
bool operator!=(const Settings &other) const { return !(*this == other); }
@@ -58,6 +69,10 @@ namespace WiringRules
bool masterEnabled();
void setMasterEnabled(bool enabled);
Settings applicationSettings();
void setApplicationSettings(const Settings &settings);
Settings effective(const Settings &project, const Settings &application);
Settings fromXml(const QDomElement &project_root);
void toXml(const Settings &settings, QDomElement &project_root);
+64 -8
View File
@@ -23,8 +23,9 @@ class tst_wiringrules : public QObject
int m_run = 0;
// Environment of one sandboxed run; @p master_off writes the master
// switch off into that run's own settings.
QProcessEnvironment sandbox(bool master_off = false)
// switch off into that run's own settings, @p application_max_wires
// the limit every project follows unless it sets its own.
QProcessEnvironment sandbox(bool master_off = false, int application_max_wires = 0)
{
const QString home = m_dir.filePath(QStringLiteral("home%1").arg(m_run));
const QString tmp = m_dir.filePath(QStringLiteral("tmp%1").arg(m_run));
@@ -33,10 +34,12 @@ class tst_wiringrules : public QObject
QDir().mkpath(home);
QDir().mkpath(tmp);
QDir().mkpath(settings + QStringLiteral("/QElectroTech"));
if (master_off) {
if (master_off || application_max_wires) {
QFile ini(settings + QStringLiteral("/QElectroTech/QElectroTech.ini"));
if (ini.open(QIODevice::WriteOnly | QIODevice::Text))
ini.write("[diagrameditor]\nwiring_rules_enabled=false\n");
ini.write(QStringLiteral("[diagrameditor]\nwiring_rules_enabled=%1\nwiring_rules_max_wires=%2\n")
.arg(master_off ? QStringLiteral("false") : QStringLiteral("true"))
.arg(application_max_wires).toUtf8());
}
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
@@ -85,7 +88,8 @@ class tst_wiringrules : public QObject
// Wires a free terminal to a terminal that already has a wire, through
// qet.addConductor(); returns what the script printed after "PROBE ".
QString addWireToWiredTerminal(const QString &project, bool master_off)
QString addWireToWiredTerminal(const QString &project, bool master_off,
int application_max_wires = 0)
{
const QString script_path = m_dir.filePath(QStringLiteral("probe%1.js").arg(m_run));
QFile script(script_path);
@@ -109,7 +113,7 @@ class tst_wiringrules : public QObject
script.close();
QProcess proc;
proc.setProcessEnvironment(sandbox(master_off));
proc.setProcessEnvironment(sandbox(master_off, application_max_wires));
proc.start(QStringLiteral(QET_TEST_BINARY_PATH), {QStringLiteral("--run"), script_path, project});
if (!proc.waitForFinished(60000))
return {};
@@ -160,18 +164,52 @@ private slots:
QVERIFY(!WiringRules::hasRoom(1, 1));
}
void projectOverridesApplication()
{
WiringRules::Settings application;
application.max_wires = 2;
application.one_wire_per_report = true;
// A project that follows the application gets its rules
WiringRules::Settings follows;
QCOMPARE(WiringRules::effective(follows, application).max_wires, 2);
QVERIFY(WiringRules::effective(follows, application).one_wire_per_report);
QVERIFY(!WiringRules::effective(follows, application).own);
// Its own rules win, "no limit" included
WiringRules::Settings own;
own.own = true;
QCOMPARE(WiringRules::effective(own, application).max_wires, 0);
QVERIFY(!WiringRules::effective(own, application).one_wire_per_report);
QVERIFY(WiringRules::effective(own, application).own);
own.max_wires = 6;
QCOMPARE(WiringRules::effective(own, application).max_wires, 6);
}
void xmlRoundTrip()
{
QDomDocument doc;
QDomElement root = doc.createElement(QStringLiteral("project"));
doc.appendChild(root);
// No rule: nothing written, and reading nothing gives no rule
WiringRules::toXml(WiringRules::Settings(), root);
// Following the application: nothing written, and reading
// nothing follows the application
WiringRules::Settings follows;
follows.max_wires = 3; // not the project's: not written
WiringRules::toXml(follows, root);
QVERIFY(root.firstChildElement().isNull());
QVERIFY(WiringRules::fromXml(root).isDefault());
// Its own rules, even "none", are written and read back as its own
WiringRules::Settings none;
none.own = true;
WiringRules::toXml(none, root);
QCOMPARE(root.firstChildElement().tagName(), QStringLiteral("wiring_rules"));
QVERIFY(WiringRules::fromXml(root) == none);
root.removeChild(root.firstChildElement());
WiringRules::Settings rules;
rules.own = true;
rules.max_wires = 2;
rules.one_wire_per_report = true;
WiringRules::toXml(rules, root);
@@ -193,6 +231,14 @@ private slots:
QVERIFY2(saved.contains(QLatin1String("max_wires_per_terminal=\"2\""))
&& saved.contains(QLatin1String("one_wire_per_report=\"true\"")),
"the setting was lost on save");
// A project's own "no rule" is kept too: it overrides the application's
const QString own_none = fixtureWith(QStringLiteral("<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()
@@ -209,6 +255,16 @@ private slots:
// The master switch off: the project's rule does nothing
QCOMPARE(addWireToWiredTerminal(limited, true), QStringLiteral("true"));
// The application's limit applies to a project that sets none...
const QString plain = QFINDTESTDATA("fixtures/qet_bug_repro_resaved.qet");
QCOMPARE(addWireToWiredTerminal(plain, false, 1), QStringLiteral("false"));
// ...not to one that sets its own, here "no limit"
const QString own_none = fixtureWith(QStringLiteral("<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"));
}
};