diff --git a/cmake/qet_compilation_vars.cmake b/cmake/qet_compilation_vars.cmake
index 4593d5d56..9b1d9ab2f 100644
--- a/cmake/qet_compilation_vars.cmake
+++ b/cmake/qet_compilation_vars.cmake
@@ -188,6 +188,7 @@ set(QET_SRC_FILES
${QET_DIR}/sources/conductorprofile.h
${QET_DIR}/sources/conductorproperties.cpp
${QET_DIR}/sources/conductorproperties.h
+ ${QET_DIR}/sources/conductormultiedit.h
${QET_DIR}/sources/conductorsegment.cpp
${QET_DIR}/sources/conductorsegment.h
${QET_DIR}/sources/conductorsegmentprofile.h
diff --git a/sources/conductormultiedit.h b/sources/conductormultiedit.h
new file mode 100644
index 000000000..e234feae6
--- /dev/null
+++ b/sources/conductormultiedit.h
@@ -0,0 +1,213 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ 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 .
+*/
+#ifndef CONDUCTORMULTIEDIT_H
+#define CONDUCTORMULTIEDIT_H
+
+#include "QPropertyUndoCommand/qpropertyundocommand.h"
+#include "conductorproperties.h"
+
+#include
+#include
+#include
+#include
+
+#include
+
+/**
+ @brief ConductorMultiEdit
+ The rules for editing several conductors at once in the Selection
+ properties panel (#500). They are templates over the conductor type so
+ they can be tested without a diagram: T needs properties(), a
+ "properties" Q_PROPERTY, and relatedPotentialConductors() returning
+ QSet.
+*/
+namespace ConductorMultiEdit
+{
+ /**
+ @brief sortByPosition
+ Sort top to bottom, then left to right, so the panel shows the same
+ "first" conductor whatever order the scene lists the selection in.
+ @param list : the conductors
+ @param pos_of : callable giving a conductor's position
+ */
+ template
+ void sortByPosition(QList &list, PosOf pos_of)
+ {
+ std::stable_sort(list.begin(), list.end(), [&pos_of](T *a, T *b) {
+ const QPointF pa = pos_of(a);
+ const QPointF pb = pos_of(b);
+ if (pa.y() != pb.y())
+ return pa.y() < pb.y();
+ return pa.x() < pb.x();
+ });
+ }
+
+ /**
+ @brief targets
+ @param selected : the edited conductors
+ @param apply_all : true to add every conductor of their potentials
+ @return each conductor an edit reaches, once
+ */
+ template
+ QList targets(const QList &selected, bool apply_all)
+ {
+ QList list;
+ QSet seen;
+ for (T *conductor : selected)
+ {
+ //Already reached through an earlier conductor's potential:
+ //its own potential is the same one.
+ if (!conductor || seen.contains(conductor))
+ continue;
+ seen.insert(conductor);
+ list << conductor;
+ if (!apply_all)
+ continue;
+ const QSet potential = conductor->relatedPotentialConductors();
+ for (T *other : potential)
+ {
+ if (!seen.contains(other))
+ {
+ seen.insert(other);
+ list << other;
+ }
+ }
+ }
+ return list;
+ }
+
+ /**
+ @brief undo
+ @param targets : the conductors to change, see targets()
+ @param shown : the properties as the panel showed them
+ @param edited : the properties after the user's edit
+ @return one undo step giving each target the fields that differ
+ between shown and edited, or nullptr if no conductor changes
+ */
+ template
+ QUndoCommand *undo(const QList &targets,
+ const ConductorProperties &shown,
+ const ConductorProperties &edited)
+ {
+ if (edited == shown)
+ return nullptr;
+
+ auto *undo = new QUndoCommand();
+ int changed = 0;
+ for (T *conductor : targets)
+ {
+ const ConductorProperties old_properties = conductor->properties();
+ ConductorProperties properties = old_properties;
+ properties.applyChanges(shown, edited);
+ if (properties == old_properties)
+ continue;
+
+ QVariant old_value, new_value;
+ old_value.setValue(old_properties);
+ new_value.setValue(properties);
+ new QPropertyUndoCommand(conductor, "properties", old_value, new_value, undo);
+ ++changed;
+ }
+
+ if (!changed)
+ {
+ delete undo;
+ return nullptr;
+ }
+ undo->setText(changed == 1
+ ? QCoreApplication::translate("ConductorPropertiesEditorWidget",
+ "Modifier les propriétés d'un conducteur", "undo caption")
+ : QCoreApplication::translate("ConductorPropertiesEditorWidget",
+ "Modifier les propriétés de plusieurs conducteurs", "undo caption"));
+ return undo;
+ }
+
+ /// The fields a user types in. When the selected conductors do not
+ /// agree on one, the panel shows it blank.
+ enum TextField {
+ Text, Formula, Function, TensionProtocol,
+ WireColor, WireSection, Cable, Bus
+ };
+
+ inline QList textFields()
+ {
+ return {Text, Formula, Function, TensionProtocol,
+ WireColor, WireSection, Cable, Bus};
+ }
+
+ inline QString &textField(ConductorProperties &p, TextField field)
+ {
+ switch (field)
+ {
+ case Text: return p.text;
+ case Formula: return p.m_formula;
+ case Function: return p.m_function;
+ case TensionProtocol: return p.m_tension_protocol;
+ case WireColor: return p.m_wire_color;
+ case WireSection: return p.m_wire_section;
+ case Cable: return p.m_cable;
+ case Bus: break;
+ }
+ return p.m_bus;
+ }
+
+ /**
+ @brief mixedTextFields
+ @return the text fields whose value is not the same in all of list
+ */
+ inline QList mixedTextFields(QList list)
+ {
+ QList mixed;
+ if (list.size() < 2)
+ return mixed;
+ for (TextField field : textFields())
+ {
+ const QString first = textField(list.first(), field);
+ for (ConductorProperties &p : list)
+ {
+ if (textField(p, field) != first)
+ {
+ mixed << field;
+ break;
+ }
+ }
+ }
+ return mixed;
+ }
+
+ /**
+ @brief shown
+ @return what the panel shows for list: the first conductor's
+ properties, with the mixed text fields blank. Typing any value
+ in a blank field, even the first conductor's, then differs from
+ what was shown, so it is applied to every conductor.
+ */
+ inline ConductorProperties shown(const QList &list,
+ const QList &mixed)
+ {
+ if (list.isEmpty())
+ return ConductorProperties();
+ ConductorProperties p = list.first();
+ for (TextField field : mixed)
+ textField(p, field).clear();
+ return p;
+ }
+}
+
+#endif // CONDUCTORMULTIEDIT_H
diff --git a/sources/factory/propertieseditorfactory.cpp b/sources/factory/propertieseditorfactory.cpp
index 83b554f79..ba8b6acb3 100644
--- a/sources/factory/propertieseditorfactory.cpp
+++ b/sources/factory/propertieseditorfactory.cpp
@@ -38,6 +38,7 @@
#include "../ui/shapegraphicsitempropertieswidget.h"
#include
+#include
#include
/**
@@ -87,13 +88,15 @@ PropertiesEditorWidget *PropertiesEditorFactory::propertiesEditor(
//A rubber band over wires selects their labels too: each label
//counts as its conductor, once.
QList mapped;
+ QSet seen;
for (QGraphicsItem *qgi : std::as_const(items)) {
if (auto *cti = qgraphicsitem_cast(qgi)) {
if (Conductor *parent_cond = cti->parentConductor()) {
qgi = parent_cond;
}
}
- if (!mapped.contains(qgi)) {
+ if (!seen.contains(qgi)) {
+ seen.insert(qgi);
mapped << qgi;
}
}
diff --git a/sources/ui/conductorpropertieseditorwidget.cpp b/sources/ui/conductorpropertieseditorwidget.cpp
index af3e0be33..60dd4ba43 100644
--- a/sources/ui/conductorpropertieseditorwidget.cpp
+++ b/sources/ui/conductorpropertieseditorwidget.cpp
@@ -17,7 +17,7 @@
*/
#include "conductorpropertieseditorwidget.h"
-#include "../QPropertyUndoCommand/qpropertyundocommand.h"
+#include "../conductormultiedit.h"
#include "../diagram.h"
#include "../qetgraphicsitem/conductor.h"
#include "conductorpropertieswidget.h"
@@ -143,18 +143,26 @@ void ConductorPropertiesEditorWidget::setConductors(
for (const QPointer &c : std::as_const(m_conductors))
if (c)
disconnect(c, &Conductor::propertiesChange,
- this, &ConductorPropertiesEditorWidget::updateUi);
+ this, &ConductorPropertiesEditorWidget::scheduleUpdateUi);
m_conductors.clear();
- for (Conductor *c : conductors)
+ //The scene gives no order for its selection: sort, so the same
+ //wires always show the same "first" one.
+ QList sorted = conductors;
+ ConductorMultiEdit::sortByPosition(sorted, [](Conductor *c) {
+ return c->sceneBoundingRect().topLeft();
+ });
+
+ for (Conductor *c : std::as_const(sorted))
{
m_conductors << c;
//Keep the dock in sync when a conductor is edited elsewhere (e.g.
//the modal "Edit conductor" dialog); otherwise a stale snapshot
//would be written back on the next apply() and overwrite that
- //change (issue #500).
+ //change (issue #500). One edit of N conductors emits N signals:
+ //they are gathered into one reload.
connect(c, &Conductor::propertiesChange,
- this, &ConductorPropertiesEditorWidget::updateUi,
+ this, &ConductorPropertiesEditorWidget::scheduleUpdateUi,
Qt::UniqueConnection);
}
@@ -195,7 +203,21 @@ void ConductorPropertiesEditorWidget::apply()
if (!first || !first->diagram()) return;
if (QUndoCommand *undo = associatedUndo())
first->diagram()->undoStack().push(undo);
- m_shown = m_cpw->properties();
+ updateUi();
+}
+
+/**
+ @brief ConductorPropertiesEditorWidget::scheduleUpdateUi
+ Reload the widget once the current event is done, however many
+ conductors changed in it.
+*/
+void ConductorPropertiesEditorWidget::scheduleUpdateUi()
+{
+ if (m_update_pending) return;
+ m_update_pending = true;
+ QMetaObject::invokeMethod(this, [this]() {
+ if (m_update_pending) updateUi();
+ }, Qt::QueuedConnection);
}
/**
@@ -289,15 +311,24 @@ void ConductorPropertiesEditorWidget::reset()
/**
@brief ConductorPropertiesEditorWidget::updateUi
- Reload the widget from the first conductor (e.g. when the selection
- changes).
+ Reload the widget from the conductors (e.g. when the selection
+ changes): the first one's values, with the text fields they do not
+ agree on left blank.
*/
void ConductorPropertiesEditorWidget::updateUi()
{
- Conductor *first = firstConductor();
- if (!first) return;
+ m_update_pending = false;
+ QList list;
+ for (const QPointer &c : std::as_const(m_conductors))
+ if (c)
+ list << c->properties();
+ if (list.isEmpty()) return;
+
+ const auto mixed = ConductorMultiEdit::mixedTextFields(list);
m_updating = true;
- m_cpw->setProperties(first->properties());
+ m_cpw->setProperties(ConductorMultiEdit::shown(list, mixed));
+ m_cpw->setMixedTextFields(mixed);
+ m_cpw->setTextLocked(list.size() > 1);
//Read back rather than keep the conductor's own values: a value the
//widget cannot show exactly must not count as an edit.
m_shown = m_cpw->properties();
@@ -319,47 +350,20 @@ void ConductorPropertiesEditorWidget::updateUi()
*/
QUndoCommand *ConductorPropertiesEditorWidget::associatedUndo() const
{
- const ConductorProperties new_properties = m_cpw->properties();
- if (new_properties == m_shown) return nullptr;
+ //Most calls are a field losing focus with nothing edited: answer
+ //before walking the potentials.
+ const ConductorProperties edited = m_cpw->properties();
+ if (edited == m_shown) return nullptr;
- const bool apply_all = m_apply_all_cb && m_apply_all_cb->isChecked();
- QList targets;
+ QList selected;
for (const QPointer &c : m_conductors)
- {
- if (!c) continue;
- if (!targets.contains(c.data()))
- targets << c.data();
- if (apply_all)
- for (Conductor *potential_conductor : c->relatedPotentialConductors())
- if (!targets.contains(potential_conductor))
- targets << potential_conductor;
- }
+ if (c)
+ selected << c.data();
- auto *undo = new QUndoCommand();
- int changed = 0;
- for (Conductor *conductor : std::as_const(targets))
- {
- const ConductorProperties old_properties = conductor->properties();
- ConductorProperties properties = old_properties;
- properties.applyChanges(m_shown, new_properties);
- if (properties == old_properties) continue;
-
- QVariant old_value, new_value;
- old_value.setValue(old_properties);
- new_value.setValue(properties);
- new QPropertyUndoCommand(conductor, "properties", old_value, new_value, undo);
- ++changed;
- }
-
- if (!changed)
- {
- delete undo;
- return nullptr;
- }
- undo->setText(changed == 1
- ? tr("Modifier les propriétés d'un conducteur", "undo caption")
- : tr("Modifier les propriétés de plusieurs conducteurs", "undo caption"));
- return undo;
+ return ConductorMultiEdit::undo(
+ ConductorMultiEdit::targets(
+ selected, m_apply_all_cb && m_apply_all_cb->isChecked()),
+ m_shown, edited);
}
/**
diff --git a/sources/ui/conductorpropertieseditorwidget.h b/sources/ui/conductorpropertieseditorwidget.h
index f0b0d0d0c..ea915a706 100644
--- a/sources/ui/conductorpropertieseditorwidget.h
+++ b/sources/ui/conductorpropertieseditorwidget.h
@@ -65,6 +65,7 @@ class ConductorPropertiesEditorWidget : public PropertiesEditorWidget
private:
void connectChangeSignals();
void disconnectChangeSignals();
+ void scheduleUpdateUi();
Conductor *firstConductor() const;
private:
@@ -77,6 +78,7 @@ class ConductorPropertiesEditorWidget : public PropertiesEditorWidget
ConductorProperties m_shown;
QList m_live_connections;
bool m_updating = false;
+ bool m_update_pending = false;
};
#endif // CONDUCTORPROPERTIESEDITORWIDGET_H
diff --git a/sources/ui/conductorpropertieswidget.cpp b/sources/ui/conductorpropertieswidget.cpp
index e184c6133..46729cfe7 100644
--- a/sources/ui/conductorpropertieswidget.cpp
+++ b/sources/ui/conductorpropertieswidget.cpp
@@ -191,6 +191,48 @@ void ConductorPropertiesWidget::setHiddenOneTextPerFolio(const bool &hide) {
ui -> m_one_text_per_folio_cb -> setHidden(hide);
}
+/**
+ @brief ConductorPropertiesWidget::setMixedTextFields
+ Mark the text fields whose value differs between several edited
+ conductors: they are shown blank, with a hint saying why.
+ @param mixed
+*/
+void ConductorPropertiesWidget::setMixedTextFields(
+ const QList &mixed)
+{
+ using namespace ConductorMultiEdit;
+ const QList> edits {
+ {Text, ui->m_text_le},
+ {Formula, ui->m_formula_le},
+ {Function, ui->m_function_le},
+ {TensionProtocol, ui->m_tension_protocol_le},
+ {WireColor, ui->m_wire_color_le},
+ {WireSection, ui->m_wire_section_le},
+ {Cable, ui->m_cable_le},
+ {Bus, ui->m_bus_le}};
+
+ for (const auto &edit : edits)
+ edit.second->setPlaceholderText(mixed.contains(edit.first)
+ ? tr("Plusieurs valeurs", "several conductors, different values")
+ : QString());
+}
+
+/**
+ @brief ConductorPropertiesWidget::setTextLocked
+ Lock the conductor text while several conductors are edited at once:
+ the same number on all of them would make duplicates.
+ @param locked
+*/
+void ConductorPropertiesWidget::setTextLocked(bool locked)
+{
+ m_text_locked = locked;
+ ui->m_text_le->setEnabled(!locked && ui->m_formula_le->text().isEmpty());
+ ui->m_text_le->setToolTip(locked
+ ? tr("Plusieurs conducteurs sélectionnés : le texte se modifie "
+ "sur un seul conducteur à la fois.")
+ : tr("Texte"));
+}
+
/**
@brief ConductorPropertiesWidget::setDisabledShowText
@param disable
@@ -239,7 +281,7 @@ void ConductorPropertiesWidget::initWidget()
connect(ui->m_multiwires_gb, &QGroupBox::toggled, [this](bool toggle) {this->ui->m_singlewire_gb->setChecked(!toggle);});
connect(ui->m_singlewire_gb, &QGroupBox::toggled, [this](bool toggle) {this->ui->m_multiwires_gb->setChecked(!toggle);});
- connect(ui->m_formula_le, &QLineEdit::textChanged, [this](QString text) {this->ui->m_text_le->setEnabled(text.isEmpty());});
+ connect(ui->m_formula_le, &QLineEdit::textChanged, [this](QString text) {this->ui->m_text_le->setEnabled(!m_text_locked && text.isEmpty());});
ui->m_multiwires_gb->setChecked(true);
ui->m_singlewire_gb->setChecked(true);
#if TODO_LIST
diff --git a/sources/ui/conductorpropertieswidget.h b/sources/ui/conductorpropertieswidget.h
index a27ef868a..a52178269 100644
--- a/sources/ui/conductorpropertieswidget.h
+++ b/sources/ui/conductorpropertieswidget.h
@@ -18,6 +18,7 @@
#ifndef CONDUCTORPROPERTIESWIDGET_H
#define CONDUCTORPROPERTIESWIDGET_H
+#include "../conductormultiedit.h"
#include "../conductorproperties.h"
#include
@@ -49,6 +50,8 @@ class ConductorPropertiesWidget : public QWidget
void setHiddenOneTextPerFolio (const bool &hide);
void setDisabledShowText (const bool &disable = true);
void setHiddenAvailableAutonum (const bool &hide);
+ void setMixedTextFields(const QList &mixed);
+ void setTextLocked(bool locked);
QComboBox *autonumComboBox() const;
QPushButton *editAutonumPushButton() const;
@@ -72,6 +75,7 @@ class ConductorPropertiesWidget : public QWidget
Ui::ConductorPropertiesWidget *ui;
ConductorProperties m_properties;
QTextOrientationSpinBoxWidget *m_verti_select, *m_horiz_select;
+ bool m_text_locked = false;
};
#endif // CONDUCTORPROPERTIESWIDGET_H
diff --git a/tests/qttest/CMakeLists.txt b/tests/qttest/CMakeLists.txt
index e339f3721..d8bb2b612 100644
--- a/tests/qttest/CMakeLists.txt
+++ b/tests/qttest/CMakeLists.txt
@@ -523,6 +523,21 @@ add_test(NAME tst_conductorapplychanges COMMAND tst_conductorapplychanges)
target_include_directories(tst_conductorapplychanges PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_conductorapplychanges PRIVATE Qt::Test Qt::Widgets Qt::Xml pugixml::pugixml)
+# ConductorMultiEdit -- editing several wires at once in the Selection
+# properties panel: order, which wires an edit reaches, one undo step for
+# all of them, and fields shown blank because the wires disagree.
+add_executable(
+ tst_conductormultiedit
+ tst_conductormultiedit.cpp
+ ${QET_DIR}/sources/conductorproperties.cpp
+ ${QET_DIR}/sources/QPropertyUndoCommand/qpropertyundocommand.cpp
+ ${QET_DIR}/sources/qet.cpp
+ ${QET_DIR}/sources/qeticons.cpp
+ ${QET_DIR}/sources/shortcutmanager.cpp)
+add_test(NAME tst_conductormultiedit COMMAND tst_conductormultiedit)
+target_include_directories(tst_conductormultiedit PRIVATE ${QET_DIR}/sources)
+target_link_libraries(tst_conductormultiedit PRIVATE Qt::Test Qt::Widgets Qt::Xml pugixml::pugixml)
+
# DiagramContext::fromXml() -- the two readers (QDom for projects, pugixml
# for element definitions in the collection) give the same values: stray
# spaces trimmed, accents kept.
diff --git a/tests/qttest/tst_conductorapplychanges.cpp b/tests/qttest/tst_conductorapplychanges.cpp
index aa286b0a0..75be52264 100644
--- a/tests/qttest/tst_conductorapplychanges.cpp
+++ b/tests/qttest/tst_conductorapplychanges.cpp
@@ -102,9 +102,11 @@ class tst_conductorapplychanges : public QObject
QTest::newRow(list.at(i).first) << i;
}
- // The shown wire is all variant 0, the user changes one field
- // to variant 1, another selected wire is all variant 2: it
- // takes that one field and keeps every other one.
+ // The shown wire is all variant 0 and the user changes one
+ // field to variant 1. The other wire holds variant 1 in every
+ // other field, so copying one of them by mistake (they are
+ // variant 0 in the edit) shows, booleans included; and variant
+ // 2 in the edited field, so not copying it shows too.
void eachFieldAlone()
{
QFETCH(int, index);
@@ -115,9 +117,11 @@ class tst_conductorapplychanges : public QObject
set(after, 1);
QVERIFY(after != before);
- ConductorProperties other = all(2);
+ ConductorProperties other = all(1);
+ set(other, 2);
ConductorProperties expected = other;
set(expected, 1);
+ QVERIFY(expected != other);
other.applyChanges(before, after);
QVERIFY(other == expected);
@@ -127,7 +131,7 @@ class tst_conductorapplychanges : public QObject
void noEditChangesNothing()
{
const ConductorProperties shown = all(0);
- ConductorProperties other = all(2);
+ ConductorProperties other = all(1);
const ConductorProperties kept = other;
other.applyChanges(shown, shown);
QVERIFY(other == kept);
diff --git a/tests/qttest/tst_conductormultiedit.cpp b/tests/qttest/tst_conductormultiedit.cpp
new file mode 100644
index 000000000..9a6d36e92
--- /dev/null
+++ b/tests/qttest/tst_conductormultiedit.cpp
@@ -0,0 +1,189 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include
+
+#include
+
+#include "conductormultiedit.h"
+#include "qetapp.h"
+
+ // qet.cpp needs it; the application is not linked
+QString QETApp::m_interface_language;
+
+// A wire as ConductorMultiEdit sees one: properties through the same
+// Q_PROPERTY the undo command writes, a potential, and a position.
+class FakeWire : public QObject
+{
+ Q_OBJECT
+ Q_PROPERTY(ConductorProperties properties READ properties WRITE setProperties)
+
+ public:
+ FakeWire(const QString &num, const QString &function,
+ const QString &cable, QPointF pos) :
+ m_pos(pos)
+ {
+ m_properties.text = num;
+ m_properties.m_function = function;
+ m_properties.m_cable = cable;
+ }
+
+ ConductorProperties properties() const { return m_properties; }
+ void setProperties(const ConductorProperties &p) { m_properties = p; }
+ QSet relatedPotentialConductors() const { return m_potential; }
+ QPointF pos() const { return m_pos; }
+
+ // Joins wires into one potential; like the real one, a wire's
+ // potential lists the others, not itself.
+ static void join(const QList &wires)
+ {
+ for (FakeWire *w : wires)
+ for (FakeWire *o : wires)
+ if (o != w)
+ w->m_potential.insert(o);
+ }
+
+ ConductorProperties m_properties;
+
+ private:
+ QSet m_potential;
+ QPointF m_pos;
+};
+
+// The rules for editing several wires at once in the Selection properties
+// panel (#500): which wire is shown first, which wires an edit reaches,
+// what one undo step does to all of them, and the fields shown blank
+// because the wires do not agree on them.
+class tst_conductormultiedit : public QObject
+{
+ Q_OBJECT
+
+ using Wires = QList;
+
+ static ConductorProperties edited(ConductorProperties p,
+ QString ConductorProperties::*field,
+ const QString &value)
+ {
+ p.*field = value;
+ return p;
+ }
+
+ private slots:
+ // Same wires, any selection order: same first wire
+ void firstWireIsStable()
+ {
+ FakeWire a("1", "", "", {50, 10}), b("2", "", "", {10, 10}),
+ c("3", "", "", {0, 90});
+ const auto pos = [](FakeWire *w) { return w->pos(); };
+
+ Wires one {&c, &a, &b}, two {&a, &b, &c};
+ ConductorMultiEdit::sortByPosition(one, pos);
+ ConductorMultiEdit::sortByPosition(two, pos);
+ QCOMPARE(one, (Wires {&b, &a, &c}));
+ QCOMPARE(two, one);
+ }
+
+ void targetsWithoutPotential()
+ {
+ FakeWire a("1", "", "", {}), b("2", "", "", {}), c("3", "", "", {});
+ FakeWire::join({&a, &c});
+ QCOMPARE(ConductorMultiEdit::targets(Wires {&a, &b, &a}, false),
+ (Wires {&a, &b}));
+ }
+
+ // Each wire once: a selected wire already reached through
+ // another one's potential is not added again
+ void targetsWithPotential()
+ {
+ FakeWire a("1", "", "", {}), b("1", "", "", {}), c("1", "", "", {}),
+ d("2", "", "", {});
+ FakeWire::join({&a, &b, &c});
+ const Wires t = ConductorMultiEdit::targets(Wires {&a, &d, &c}, true);
+ QCOMPARE(t.size(), 4);
+ QCOMPARE(QSet(t.begin(), t.end()),
+ (QSet {&a, &b, &c, &d}));
+ }
+
+ // One edit of three wires: each takes the function, keeps its
+ // own number and cable; one undo restores all three exactly
+ void undoRedoThreeWires()
+ {
+ FakeWire a("101", "fnA", "W1", {}), b("102", "fnB", "W2", {}),
+ c("103", "fnC", "W3", {});
+ const Wires wires {&a, &b, &c};
+ const ConductorProperties before_a = a.properties(),
+ before_b = b.properties(), before_c = c.properties();
+
+ const ConductorProperties shown = a.properties();
+ QUndoStack stack;
+ stack.push(ConductorMultiEdit::undo(wires, shown,
+ edited(shown, &ConductorProperties::m_function, "PWR")));
+ QCOMPARE(stack.count(), 1);
+
+ for (FakeWire *w : wires)
+ QCOMPARE(w->m_properties.m_function, QStringLiteral("PWR"));
+ QCOMPARE(b.m_properties.text, QStringLiteral("102"));
+ QCOMPARE(c.m_properties.m_cable, QStringLiteral("W3"));
+
+ stack.undo();
+ QVERIFY(a.properties() == before_a);
+ QVERIFY(b.properties() == before_b);
+ QVERIFY(c.properties() == before_c);
+
+ stack.redo();
+ QCOMPARE(c.m_properties.m_function, QStringLiteral("PWR"));
+ QCOMPARE(c.m_properties.text, QStringLiteral("103"));
+ }
+
+ void noUndoWhenNothingChanges()
+ {
+ FakeWire a("1", "PWR", "", {}), b("2", "PWR", "", {});
+ const ConductorProperties shown = a.properties();
+ QCOMPARE(ConductorMultiEdit::undo(Wires {&a, &b}, shown, shown),
+ static_cast(nullptr));
+ // An edit every wire already has
+ QCOMPARE(ConductorMultiEdit::undo(Wires {&a, &b}, edited(shown,
+ &ConductorProperties::m_function, "X"), shown),
+ static_cast(nullptr));
+ }
+
+ // Wires that disagree on a text field show it blank; typing the
+ // first wire's own value then reaches every wire (1.5 mm² on
+ // a 1.5 and a 2.5 wire)
+ void mixedFieldTakesFirstWiresValue()
+ {
+ FakeWire a("1", "fn", "", {}), b("2", "fn", "", {});
+ a.m_properties.m_wire_section = "1.5";
+ b.m_properties.m_wire_section = "2.5";
+ const QList list {a.properties(), b.properties()};
+
+ const auto mixed = ConductorMultiEdit::mixedTextFields(list);
+ QCOMPARE(mixed, (QList {
+ ConductorMultiEdit::Text, ConductorMultiEdit::WireSection}));
+
+ const ConductorProperties shown = ConductorMultiEdit::shown(list, mixed);
+ QVERIFY(shown.m_wire_section.isEmpty());
+ QCOMPARE(shown.m_function, QStringLiteral("fn"));
+
+ QUndoStack stack;
+ stack.push(ConductorMultiEdit::undo(Wires {&a, &b}, shown,
+ edited(shown, &ConductorProperties::m_wire_section, "1.5")));
+ QCOMPARE(a.m_properties.m_wire_section, QStringLiteral("1.5"));
+ QCOMPARE(b.m_properties.m_wire_section, QStringLiteral("1.5"));
+ // Another field edited while the section is mixed leaves
+ // each wire's section alone
+ a.m_properties.m_wire_section = "1.5";
+ b.m_properties.m_wire_section = "2.5";
+ stack.push(ConductorMultiEdit::undo(Wires {&a, &b}, shown,
+ edited(shown, &ConductorProperties::m_function, "PWR")));
+ QCOMPARE(a.m_properties.m_wire_section, QStringLiteral("1.5"));
+ QCOMPARE(b.m_properties.m_wire_section, QStringLiteral("2.5"));
+ }
+
+ void oneWireIsNeverMixed()
+ {
+ FakeWire a("1", "fn", "", {});
+ QVERIFY(ConductorMultiEdit::mixedTextFields({a.properties()}).isEmpty());
+ }
+};
+
+QTEST_GUILESS_MAIN(tst_conductormultiedit)
+#include "tst_conductormultiedit.moc"