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"