diff --git a/sources/qetproject.cpp b/sources/qetproject.cpp index 4a20fd926..e8ce5998d 100644 --- a/sources/qetproject.cpp +++ b/sources/qetproject.cpp @@ -1260,16 +1260,27 @@ void QETProject::setWireHops(WireHops::Mode mode) /** @brief QETProject::wiringRules @return how many wires a terminal of this project may take - (discussion #1158); the default when the project sets no rule. + (discussion #1158): the project's own rules when it sets them, + otherwise the application's (Settings > General). */ WiringRules::Settings QETProject::wiringRules() const { + return WiringRules::effective(m_wiring_rules, WiringRules::applicationSettings()); +} + +/** + @brief QETProject::projectWiringRules + @return the rules as the project stores them: Settings::own false when + it follows the application's. +*/ +WiringRules::Settings QETProject::projectWiringRules() const { return m_wiring_rules; } /** @brief QETProject::setWiringRules - Set how many wires a terminal of this project may take. Only wires - drawn from now on are affected: none already drawn is removed. + Set the project's own wiring rules, or (Settings::own false) make it + follow the application's. Only wires drawn from now on are affected: + none already drawn is removed. @param rules */ void QETProject::setWiringRules(const WiringRules::Settings &rules) diff --git a/sources/qetproject.h b/sources/qetproject.h index 2f713919e..90fcbd0fb 100644 --- a/sources/qetproject.h +++ b/sources/qetproject.h @@ -213,6 +213,7 @@ class QETProject : public QObject WireHops::Mode wireHops() const; void setWireHops(WireHops::Mode mode); WiringRules::Settings wiringRules() const; + WiringRules::Settings projectWiringRules() const; void setWiringRules(const WiringRules::Settings &rules); void setAutoBreakConductor (bool abc); void setAutoElement (bool ae); diff --git a/sources/ui/configpage/generalconfigurationpage.cpp b/sources/ui/configpage/generalconfigurationpage.cpp index 7a6293b81..aa6e63989 100644 --- a/sources/ui/configpage/generalconfigurationpage.cpp +++ b/sources/ui/configpage/generalconfigurationpage.cpp @@ -115,6 +115,19 @@ GeneralConfigurationPage::GeneralConfigurationPage(QWidget *parent) : ui->m_zoom_out_beyond_folio->setChecked(settings.value("diagrameditor/zoom-out-beyond-of-folio", false).toBool()); ui->m_conductor_properties_panel->setChecked(settings.value("diagrameditor/conductor_properties_panel", false).toBool()); ui->m_wiring_rules_cb->setChecked(WiringRules::masterEnabled()); + { + //The rules every project follows unless it sets its own (#1158) + const WiringRules::Settings rules = WiringRules::applicationSettings(); + ui->m_wiring_max_wires_sb->setValue(rules.max_wires); + ui->m_wiring_one_wire_per_report_cb->setChecked(rules.one_wire_per_report); + auto enable = [this](bool on) { + ui->m_wiring_max_wires_label->setEnabled(on); + ui->m_wiring_max_wires_sb->setEnabled(on); + ui->m_wiring_one_wire_per_report_cb->setEnabled(on); + }; + enable(ui->m_wiring_rules_cb->isChecked()); + connect(ui->m_wiring_rules_cb, &QCheckBox::toggled, this, enable); + } ui->m_use_gesture_trackpad->setChecked(settings.value("diagramview/gestures", false).toBool()); ui->m_save_label_paste->setChecked(settings.value("diagramcommands/erase-label-on-copy", true).toBool()); ui->m_enable_scripting->setChecked(QetSettings::scriptingEnabled()); @@ -337,6 +350,12 @@ void GeneralConfigurationPage::applyConf() settings.setValue("diagrameditor/zoom-out-beyond-of-folio", ui->m_zoom_out_beyond_folio->isChecked()); settings.setValue("diagrameditor/conductor_properties_panel", ui->m_conductor_properties_panel->isChecked()); WiringRules::setMasterEnabled(ui->m_wiring_rules_cb->isChecked()); + { + WiringRules::Settings rules = WiringRules::applicationSettings(); + rules.max_wires = ui->m_wiring_max_wires_sb->value(); + rules.one_wire_per_report = ui->m_wiring_one_wire_per_report_cb->isChecked(); + WiringRules::setApplicationSettings(rules); + } settings.setValue("diagrameditor/autosave-interval", ui->m_autosave_sb->value()); settings.setValue("diagrameditor/grid_display_startup", ui->grid_startup_cb->isChecked()); diff --git a/sources/ui/configpage/generalconfigurationpage.ui b/sources/ui/configpage/generalconfigurationpage.ui index adf7e3a21..1120faa20 100644 --- a/sources/ui/configpage/generalconfigurationpage.ui +++ b/sources/ui/configpage/generalconfigurationpage.ui @@ -83,6 +83,57 @@ + + + + 20 + + + + + Nombre maximal de conducteurs par borne : + + + + + + + Pour tous les projets, sauf ceux qui ont leurs propres réglages. Un nouveau conducteur qui dépasserait ce nombre sur une borne est refusé ; les conducteurs déjà dessinés ne sont pas modifiés. 4 correspond à deux embouts doubles, un de chaque côté de la vis. + + + Sans limite + + + 99 + + + + + + + Qt::Horizontal + + + + + + + + + 20 + + + + + Un renvoi de folio est un point virtuel : il ne reçoit qu'un conducteur, celui qui continue sur l'autre folio. + + + Un seul conducteur par renvoi de folio + + + + + @@ -1311,6 +1362,8 @@ Vous pouvez spécifier ici la valeur par défaut de ce champ pour les éléments m_zoom_out_beyond_folio m_conductor_properties_panel m_wiring_rules_cb + m_wiring_max_wires_sb + m_wiring_one_wire_per_report_cb m_use_windows_mode_rb m_use_tab_mode_rb m_save_label_paste diff --git a/sources/ui/configpage/projectconfigpages.cpp b/sources/ui/configpage/projectconfigpages.cpp index d1ac9520d..4531ffcea 100644 --- a/sources/ui/configpage/projectconfigpages.cpp +++ b/sources/ui/configpage/projectconfigpages.cpp @@ -177,10 +177,14 @@ void ProjectMainConfigPage::applyProjectConf() modified_project = true; } - WiringRules::Settings wiring_rules = m_project -> wiringRules(); - wiring_rules.max_wires = max_wires_sb_ -> value(); - wiring_rules.one_wire_per_report = one_wire_per_report_cb_ -> isChecked(); - if (m_project -> wiringRules() != wiring_rules) { + //Following the application's rules stores nothing in the project + WiringRules::Settings wiring_rules; + if (!use_application_rules_cb_ -> isChecked()) { + wiring_rules.own = true; + wiring_rules.max_wires = max_wires_sb_ -> value(); + wiring_rules.one_wire_per_report = one_wire_per_report_cb_ -> isChecked(); + } + if (m_project -> projectWiringRules() != wiring_rules) { m_project -> setWiringRules(wiring_rules); modified_project = true; } @@ -237,6 +241,20 @@ void ProjectMainConfigPage::initWidgets() //How many wires a terminal may take (discussion #1158) wiring_rules_gb_ = new QGroupBox(tr("Conducteurs par borne", "group box title")); + use_application_rules_cb_ = new QCheckBox(tr("Utiliser les réglages de l'application", "checkbox label")); + use_application_rules_cb_ -> setToolTip(tr("Les réglages de Configurer QElectroTech > Général s'appliquent. " + "Décochez pour donner à ce projet ses propres réglages, enregistrés dans le projet.", + "tooltip")); + connect(use_application_rules_cb_, &QCheckBox::toggled, this, [this](bool use) { + //Show the values that will apply: the application's, or the + //application's as a starting point for the project's own + if (use) { + const WiringRules::Settings application = WiringRules::applicationSettings(); + max_wires_sb_ -> setValue(application.max_wires); + one_wire_per_report_cb_ -> setChecked(application.one_wire_per_report); + } + updateWiringRulesWidgets(); + }); max_wires_label_ = new QLabel(tr("Nombre maximal de conducteurs par borne :", "label when configuring")); max_wires_sb_ = new QSpinBox(); max_wires_sb_ -> setRange(0, 99); @@ -288,6 +306,7 @@ void ProjectMainConfigPage::initLayout() main_layout0 -> addSpacing(10); QVBoxLayout *wiring_rules_layout = new QVBoxLayout(wiring_rules_gb_); + wiring_rules_layout -> addWidget(use_application_rules_cb_); QHBoxLayout *max_wires_layout = new QHBoxLayout(); max_wires_layout -> addWidget(max_wires_label_); max_wires_layout -> addWidget(max_wires_sb_); @@ -320,16 +339,33 @@ void ProjectMainConfigPage::readValuesFromProject() const int wire_hops_index = wire_hops_cb_ -> findData(WireHops::toString(m_project -> wireHops())); wire_hops_cb_ -> setCurrentIndex(qMax(0, wire_hops_index)); + //The rules that apply: the project's own or the application's const WiringRules::Settings wiring_rules = m_project -> wiringRules(); + { + const QSignalBlocker blocker(use_application_rules_cb_); + use_application_rules_cb_ -> setChecked(!wiring_rules.own); + } max_wires_sb_ -> setValue(wiring_rules.max_wires); one_wire_per_report_cb_ -> setChecked(wiring_rules.one_wire_per_report); + updateWiringRulesWidgets(); +} + +/** + @brief ProjectMainConfigPage::updateWiringRulesWidgets + Enable the wiring rule fields only when they can change something: the + master switch is on and the project sets its own rules. +*/ +void ProjectMainConfigPage::updateWiringRulesWidgets() +{ //The master switch wins over every project: say so rather than //let the user set a rule that does nothing. const bool master = WiringRules::masterEnabled(); + const bool own = master && !use_application_rules_cb_ -> isChecked(); wiring_rules_off_label_ -> setVisible(!master); - max_wires_label_ -> setEnabled(master); - max_wires_sb_ -> setEnabled(master); - one_wire_per_report_cb_ -> setEnabled(master); + use_application_rules_cb_ -> setEnabled(master); + max_wires_label_ -> setEnabled(own); + max_wires_sb_ -> setEnabled(own); + one_wire_per_report_cb_ -> setEnabled(own); } /** diff --git a/sources/ui/configpage/projectconfigpages.h b/sources/ui/configpage/projectconfigpages.h index 0868815d1..e911b5a43 100644 --- a/sources/ui/configpage/projectconfigpages.h +++ b/sources/ui/configpage/projectconfigpages.h @@ -116,6 +116,7 @@ class ProjectMainConfigPage : public ProjectConfigPage { private slots: void resetUsageTracker(); + void updateWiringRulesWidgets(); // attributes protected: @@ -131,6 +132,7 @@ class ProjectMainConfigPage : public ProjectConfigPage { QLabel *wire_hops_label_; QComboBox *wire_hops_cb_; QGroupBox *wiring_rules_gb_; + QCheckBox *use_application_rules_cb_; QLabel *max_wires_label_; QSpinBox *max_wires_sb_; QCheckBox *one_wire_per_report_cb_; diff --git a/sources/wiringrules.cpp b/sources/wiringrules.cpp index fcc3a66a2..d0d4e5fc8 100644 --- a/sources/wiringrules.cpp +++ b/sources/wiringrules.cpp @@ -30,6 +30,9 @@ namespace { //The master switch is read on every wire drawn, so it is read from //the settings once and kept; setMasterEnabled() keeps both in step. int master_cache = -1; + //Same for the application's rules + bool application_cached = false; + WiringRules::Settings application_cache; } /** @@ -57,11 +60,56 @@ void WiringRules::setMasterEnabled(bool enabled) master_cache = enabled ? 1 : 0; } +/** + @brief WiringRules::applicationSettings + @return the rules set for every project in Settings > General; a + project that sets its own (Settings::own) uses those instead. +*/ +WiringRules::Settings WiringRules::applicationSettings() +{ + if (!application_cached) { + const QSettings settings; + application_cache = Settings(); + application_cache.max_wires = qMax(0, settings.value(max_wires_key, 0).toInt()); + application_cache.one_wire_per_report = settings.value(one_wire_per_report_key, false).toBool(); + application_cached = true; + } + return application_cache; +} + +/** + @brief WiringRules::setApplicationSettings + Save the rules set for every project. + @param settings +*/ +void WiringRules::setApplicationSettings(const Settings &settings) +{ + QSettings qsettings; + qsettings.setValue(max_wires_key, qMax(0, settings.max_wires)); + qsettings.setValue(one_wire_per_report_key, settings.one_wire_per_report); + application_cache = settings; + application_cache.own = false; + application_cached = true; +} + +/** + @brief WiringRules::effective + @return the rules that apply to a project: its own when it sets them, + otherwise the application's. +*/ +WiringRules::Settings WiringRules::effective(const Settings &project, const Settings &application) +{ + Settings result = project.own ? project : application; + result.own = project.own; + return result; +} + /** @brief WiringRules::fromXml @param project_root : the root element of a project @return the rules stored in the child of \a project_root, - the default (no rule) when there is none. + which the project then uses instead of the application's (Settings::own); + the default, which follows the application, when there is none. */ WiringRules::Settings WiringRules::fromXml(const QDomElement &project_root) { @@ -70,6 +118,7 @@ WiringRules::Settings WiringRules::fromXml(const QDomElement &project_root) if (rules.isNull()) { return settings; } + settings.own = true; settings.max_wires = qMax(0, rules.attribute(max_wires_attribute, QStringLiteral("0")).toInt()); settings.one_wire_per_report = rules.attribute(report_attribute) == QLatin1String("true"); return settings; @@ -77,13 +126,13 @@ WiringRules::Settings WiringRules::fromXml(const QDomElement &project_root) /** @brief WiringRules::toXml - Write \a settings as a child of \a project_root. Nothing - is written when no rule is on, so a project that never used them saves - exactly as before. + Write \a settings as a child of \a project_root, only + when the project sets its own rules: a project that follows the + application's saves exactly as before. */ void WiringRules::toXml(const Settings &settings, QDomElement &project_root) { - if (settings.isDefault()) { + if (!settings.own) { return; } QDomElement rules = project_root.ownerDocument().createElement(element_name); diff --git a/sources/wiringrules.h b/sources/wiringrules.h index f9cab9247..af6311886 100644 --- a/sources/wiringrules.h +++ b/sources/wiringrules.h @@ -29,7 +29,10 @@ class QDomElement; limit, a new wire that would go past it is refused. Wires already in the drawing are never touched. - Every rule is off unless the project turns it on, and the application + The rules are set once for the application (Settings > General, + applicationSettings()) and apply to every project; a project can set + its own instead (Settings::own), saved in the project so it travels + with the drawing. Every rule is off by default, and the application has a master switch (masterEnabled()) that turns all of them off in every project at once. Kept free of any QGraphicsItem so the rules can be tested on their own. @@ -38,10 +41,17 @@ namespace WiringRules { ///The QSettings key of the master switch constexpr const char *settings_key = "diagrameditor/wiring_rules_enabled"; + ///The QSettings keys of the application's rules + constexpr const char *max_wires_key = "diagrameditor/wiring_rules_max_wires"; + constexpr const char *one_wire_per_report_key = "diagrameditor/wiring_rules_one_wire_per_report"; - ///The rules a project chose. The default is "no rule". + ///A set of rules: the application's, or a project's. The default is + ///"no rule". struct Settings { + ///For a project: it uses these values rather than the + ///application's. Ignored in the application's own settings. + bool own = false; ///Most wires a terminal may take, 0 for no limit int max_wires = 0; ///A folio report takes one wire only @@ -49,7 +59,8 @@ namespace WiringRules bool isDefault() const { return *this == Settings(); } bool operator==(const Settings &other) const { - return max_wires == other.max_wires + return own == other.own + && max_wires == other.max_wires && one_wire_per_report == other.one_wire_per_report; } bool operator!=(const Settings &other) const { return !(*this == other); } @@ -58,6 +69,10 @@ namespace WiringRules bool masterEnabled(); void setMasterEnabled(bool enabled); + Settings applicationSettings(); + void setApplicationSettings(const Settings &settings); + Settings effective(const Settings &project, const Settings &application); + Settings fromXml(const QDomElement &project_root); void toXml(const Settings &settings, QDomElement &project_root); diff --git a/tests/qttest/tst_wiringrules.cpp b/tests/qttest/tst_wiringrules.cpp index 79353cf26..b53b6073e 100644 --- a/tests/qttest/tst_wiringrules.cpp +++ b/tests/qttest/tst_wiringrules.cpp @@ -23,8 +23,9 @@ class tst_wiringrules : public QObject int m_run = 0; // Environment of one sandboxed run; @p master_off writes the master - // switch off into that run's own settings. - QProcessEnvironment sandbox(bool master_off = false) + // switch off into that run's own settings, @p application_max_wires + // the limit every project follows unless it sets its own. + QProcessEnvironment sandbox(bool master_off = false, int application_max_wires = 0) { const QString home = m_dir.filePath(QStringLiteral("home%1").arg(m_run)); const QString tmp = m_dir.filePath(QStringLiteral("tmp%1").arg(m_run)); @@ -33,10 +34,12 @@ class tst_wiringrules : public QObject QDir().mkpath(home); QDir().mkpath(tmp); QDir().mkpath(settings + QStringLiteral("/QElectroTech")); - if (master_off) { + if (master_off || application_max_wires) { QFile ini(settings + QStringLiteral("/QElectroTech/QElectroTech.ini")); if (ini.open(QIODevice::WriteOnly | QIODevice::Text)) - ini.write("[diagrameditor]\nwiring_rules_enabled=false\n"); + ini.write(QStringLiteral("[diagrameditor]\nwiring_rules_enabled=%1\nwiring_rules_max_wires=%2\n") + .arg(master_off ? QStringLiteral("false") : QStringLiteral("true")) + .arg(application_max_wires).toUtf8()); } QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen")); @@ -85,7 +88,8 @@ class tst_wiringrules : public QObject // Wires a free terminal to a terminal that already has a wire, through // qet.addConductor(); returns what the script printed after "PROBE ". - QString addWireToWiredTerminal(const QString &project, bool master_off) + QString addWireToWiredTerminal(const QString &project, bool master_off, + int application_max_wires = 0) { const QString script_path = m_dir.filePath(QStringLiteral("probe%1.js").arg(m_run)); QFile script(script_path); @@ -109,7 +113,7 @@ class tst_wiringrules : public QObject script.close(); QProcess proc; - proc.setProcessEnvironment(sandbox(master_off)); + proc.setProcessEnvironment(sandbox(master_off, application_max_wires)); proc.start(QStringLiteral(QET_TEST_BINARY_PATH), {QStringLiteral("--run"), script_path, project}); if (!proc.waitForFinished(60000)) return {}; @@ -160,18 +164,52 @@ private slots: QVERIFY(!WiringRules::hasRoom(1, 1)); } + void projectOverridesApplication() + { + WiringRules::Settings application; + application.max_wires = 2; + application.one_wire_per_report = true; + + // A project that follows the application gets its rules + WiringRules::Settings follows; + QCOMPARE(WiringRules::effective(follows, application).max_wires, 2); + QVERIFY(WiringRules::effective(follows, application).one_wire_per_report); + QVERIFY(!WiringRules::effective(follows, application).own); + + // Its own rules win, "no limit" included + WiringRules::Settings own; + own.own = true; + QCOMPARE(WiringRules::effective(own, application).max_wires, 0); + QVERIFY(!WiringRules::effective(own, application).one_wire_per_report); + QVERIFY(WiringRules::effective(own, application).own); + own.max_wires = 6; + QCOMPARE(WiringRules::effective(own, application).max_wires, 6); + } + void xmlRoundTrip() { QDomDocument doc; QDomElement root = doc.createElement(QStringLiteral("project")); doc.appendChild(root); - // No rule: nothing written, and reading nothing gives no rule - WiringRules::toXml(WiringRules::Settings(), root); + // Following the application: nothing written, and reading + // nothing follows the application + WiringRules::Settings follows; + follows.max_wires = 3; // not the project's: not written + WiringRules::toXml(follows, root); QVERIFY(root.firstChildElement().isNull()); QVERIFY(WiringRules::fromXml(root).isDefault()); + // Its own rules, even "none", are written and read back as its own + WiringRules::Settings none; + none.own = true; + WiringRules::toXml(none, root); + QCOMPARE(root.firstChildElement().tagName(), QStringLiteral("wiring_rules")); + QVERIFY(WiringRules::fromXml(root) == none); + root.removeChild(root.firstChildElement()); + WiringRules::Settings rules; + rules.own = true; rules.max_wires = 2; rules.one_wire_per_report = true; WiringRules::toXml(rules, root); @@ -193,6 +231,14 @@ private slots: QVERIFY2(saved.contains(QLatin1String("max_wires_per_terminal=\"2\"")) && saved.contains(QLatin1String("one_wire_per_report=\"true\"")), "the setting was lost on save"); + + // A project's own "no rule" is kept too: it overrides the application's + const QString own_none = fixtureWith(QStringLiteral("")); + QVERIFY(!own_none.isEmpty()); + const QString saved_none = resave(own_none); + QVERIFY2(!saved_none.isEmpty(), "--resave failed"); + QVERIFY2(saved_none.contains(QLatin1String("")), + "a project's own rules were lost on save"); } void wirePastTheLimitIsRefused() @@ -209,6 +255,16 @@ private slots: // The master switch off: the project's rule does nothing QCOMPARE(addWireToWiredTerminal(limited, true), QStringLiteral("true")); + + // The application's limit applies to a project that sets none... + const QString plain = QFINDTESTDATA("fixtures/qet_bug_repro_resaved.qet"); + QCOMPARE(addWireToWiredTerminal(plain, false, 1), QStringLiteral("false")); + // ...not to one that sets its own, here "no limit" + const QString own_none = fixtureWith(QStringLiteral("")); + QVERIFY(!own_none.isEmpty()); + QCOMPARE(addWireToWiredTerminal(own_none, false, 1), QStringLiteral("true")); + // ...and the master switch still turns it off + QCOMPARE(addWireToWiredTerminal(plain, true, 1), QStringLiteral("true")); } };