Multi-wire panel: stable first wire, mixed fields, locked text, one reload

Review of #1205:

- The scene gives no order for its selection, so the "first" wire whose
  values the panel shows could change between two selections of the
  same wires. The wires are now sorted top to bottom, then left to right.
- A text field the selected wires do not agree on (function, section,
  cable...) is shown blank with the hint "Plusieurs valeurs". Typing any
  value there, including the first wire's own, now differs from what was
  shown, so it reaches every wire: 1.5 mm² on a 1.5 and a 2.5 wire works.
- The conductor text is locked while several wires are selected: the same
  number on all of them would make duplicates. "Apply to all conductors
  of this potential" with one wire selected still sets the number on the
  whole potential.
- One edit of N wires emitted N signals, each reloading the panel. They
  are now gathered into one reload, which matters since a reload reads
  every selected wire to find the mixed fields. Selections and targets
  are de-duplicated with sets, and a wire reached through an earlier
  wire's potential is not walked again. An edit that changes nothing
  returns before any potential is walked.

The rules move to sources/conductormultiedit.h, templates over the wire
type, so tst_conductormultiedit can check them against a fake wire:
order, targets with and without potentials, one undo step and its redo
for three wires, and the mixed fields. tst_conductorapplychanges now
gives the other wire the opposite value in every boolean field, so
copying one by mistake fails the test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ispyisail
2026-10-02 06:20:17 +13:00
parent 0ed9e7eb59
commit 1c5fd5a284
10 changed files with 533 additions and 56 deletions
+1
View File
@@ -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
+213
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#ifndef CONDUCTORMULTIEDIT_H
#define CONDUCTORMULTIEDIT_H
#include "QPropertyUndoCommand/qpropertyundocommand.h"
#include "conductorproperties.h"
#include <QCoreApplication>
#include <QList>
#include <QPointF>
#include <QSet>
#include <algorithm>
/**
@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<T *>.
*/
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 <typename T, typename PosOf>
void sortByPosition(QList<T *> &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 <typename T>
QList<T *> targets(const QList<T *> &selected, bool apply_all)
{
QList<T *> list;
QSet<T *> 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<T *> 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 <typename T>
QUndoCommand *undo(const QList<T *> &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<TextField> 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<TextField> mixedTextFields(QList<ConductorProperties> list)
{
QList<TextField> 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<ConductorProperties> &list,
const QList<TextField> &mixed)
{
if (list.isEmpty())
return ConductorProperties();
ConductorProperties p = list.first();
for (TextField field : mixed)
textField(p, field).clear();
return p;
}
}
#endif // CONDUCTORMULTIEDIT_H
+4 -1
View File
@@ -38,6 +38,7 @@
#include "../ui/shapegraphicsitempropertieswidget.h"
#include <QGraphicsItem>
#include <QSet>
#include <QSettings>
/**
@@ -87,13 +88,15 @@ PropertiesEditorWidget *PropertiesEditorFactory::propertiesEditor(
//A rubber band over wires selects their labels too: each label
//counts as its conductor, once.
QList<QGraphicsItem *> mapped;
QSet<QGraphicsItem *> seen;
for (QGraphicsItem *qgi : std::as_const(items)) {
if (auto *cti = qgraphicsitem_cast<ConductorTextItem *>(qgi)) {
if (Conductor *parent_cond = cti->parentConductor()) {
qgi = parent_cond;
}
}
if (!mapped.contains(qgi)) {
if (!seen.contains(qgi)) {
seen.insert(qgi);
mapped << qgi;
}
}
+53 -49
View File
@@ -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<Conductor> &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<Conductor *> 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<ConductorProperties> list;
for (const QPointer<Conductor> &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<Conductor *> targets;
QList<Conductor *> selected;
for (const QPointer<Conductor> &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);
}
/**
@@ -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<QMetaObject::Connection> m_live_connections;
bool m_updating = false;
bool m_update_pending = false;
};
#endif // CONDUCTORPROPERTIESEDITORWIDGET_H
+43 -1
View File
@@ -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<ConductorMultiEdit::TextField> &mixed)
{
using namespace ConductorMultiEdit;
const QList<QPair<TextField, QLineEdit *>> 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
+4
View File
@@ -18,6 +18,7 @@
#ifndef CONDUCTORPROPERTIESWIDGET_H
#define CONDUCTORPROPERTIESWIDGET_H
#include "../conductormultiedit.h"
#include "../conductorproperties.h"
#include <QWidget>
@@ -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<ConductorMultiEdit::TextField> &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
+15
View File
@@ -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.
+9 -5
View File
@@ -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);
+189
View File
@@ -0,0 +1,189 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QtTest>
#include <QUndoStack>
#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<FakeWire *> 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<FakeWire *> &wires)
{
for (FakeWire *w : wires)
for (FakeWire *o : wires)
if (o != w)
w->m_potential.insert(o);
}
ConductorProperties m_properties;
private:
QSet<FakeWire *> 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<FakeWire *>;
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<FakeWire *>(t.begin(), t.end()),
(QSet<FakeWire *> {&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<QUndoCommand *>(nullptr));
// An edit every wire already has
QCOMPARE(ConductorMultiEdit::undo(Wires {&a, &b}, edited(shown,
&ConductorProperties::m_function, "X"), shown),
static_cast<QUndoCommand *>(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<ConductorProperties> list {a.properties(), b.properties()};
const auto mixed = ConductorMultiEdit::mixedTextFields(list);
QCOMPARE(mixed, (QList<ConductorMultiEdit::TextField> {
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"