diff --git a/cmake/qet_compilation_vars.cmake b/cmake/qet_compilation_vars.cmake
index aee533ed1..66a810712 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/lang/qet_en.ts b/lang/qet_en.ts
index fc3c35a1c..312e1bc33 100644
--- a/lang/qet_en.ts
+++ b/lang/qet_en.ts
@@ -1055,6 +1055,15 @@ Note: these options DO NOT allow or block auto numberings, only their update pol
ConducteurConductor
+
+
+ %n conducteurs sélectionnés : seuls les champs modifiés leur sont appliqués.
+ selection properties panel
+
+ %n conductor selected: only the fields you change are applied to it.
+ %n conductors selected: only the fields you change are applied to them.
+
+ ConductorPropertiesWidget
@@ -1302,6 +1311,17 @@ Note: these options DO NOT allow or block auto numberings, only their update pol
Style :Style:
+
+
+ Plusieurs valeurs
+ several conductors, different values
+ Several values
+
+
+
+ Plusieurs conducteurs sélectionnés : le texte se modifie sur un seul conducteur à la fois.
+ Several conductors selected: the text is changed on one conductor at a time.
+ ConfigSaveLoaderWidget
diff --git a/lang/qet_fr.ts b/lang/qet_fr.ts
index 01e15a8c4..fccaeef64 100644
--- a/lang/qet_fr.ts
+++ b/lang/qet_fr.ts
@@ -1054,6 +1054,15 @@ Remarque: Ces options n'autorisent ou bloquent l'auto numérotation, s
Conducteur
+
+
+ %n conducteurs sélectionnés : seuls les champs modifiés leur sont appliqués.
+ selection properties panel
+
+ %n conducteur sélectionné : seuls les champs modifiés lui sont appliqués.
+ %n conducteurs sélectionnés : seuls les champs modifiés leur sont appliqués.
+
+ ConductorPropertiesWidget
@@ -1301,6 +1310,17 @@ Remarque: Ces options n'autorisent ou bloquent l'auto numérotation, s
conductor style: dashed and dotted line
+
+
+ Plusieurs valeurs
+ several conductors, different values
+
+
+
+
+ Plusieurs conducteurs sélectionnés : le texte se modifie sur un seul conducteur à la fois.
+
+ ConfigSaveLoaderWidget
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 48a39738b..ba8b6acb3 100644
--- a/sources/factory/propertieseditorfactory.cpp
+++ b/sources/factory/propertieseditorfactory.cpp
@@ -38,6 +38,7 @@
#include "../ui/shapegraphicsitempropertieswidget.h"
#include
+#include
#include
/**
@@ -82,22 +83,30 @@ PropertiesEditorWidget *PropertiesEditorFactory::propertiesEditor(
PropertiesEditorWidget *editor,
QWidget *parent)
{
+ //Selecting a conductor's text label edits its parent conductor (#500),
+ //mirroring how double-clicking the label opens the conductor dialog.
+ //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 (!seen.contains(qgi)) {
+ seen.insert(qgi);
+ mapped << qgi;
+ }
+ }
+ items = mapped;
+
const int count_ = items.size();
if (count_ == 0) {
return nullptr;
}
QGraphicsItem *item = items.first();
-
- //Selecting a conductor's text label edits its parent conductor (#500),
- //mirroring how double-clicking the label opens the conductor dialog.
- if (count_ == 1) {
- if (auto *cti = qgraphicsitem_cast(item)) {
- if (Conductor *parent_cond = cti->parentConductor()) {
- items = {parent_cond};
- item = parent_cond;
- }
- }
- }
const int type_ = item->type();
//The editor widget can only edit one item
@@ -124,18 +133,17 @@ PropertiesEditorWidget *PropertiesEditorFactory::propertiesEditor(
false).toBool()) {
return nullptr;
}
- //Prototype (#500): single-conductor editing in the dock.
- if (count_ > 1) {
- return nullptr;
+ QList conductors;
+ for (QGraphicsItem *qgi : std::as_const(items)) {
+ conductors << static_cast(qgi);
}
- auto conductor = static_cast(item);
if (class_name == ConductorPropertiesEditorWidget::staticMetaObject.className())
{
- static_cast(editor)->setConductor(conductor);
+ static_cast(editor)->setConductors(conductors);
return editor;
}
- return new ConductorPropertiesEditorWidget(conductor, parent);
+ return new ConductorPropertiesEditorWidget(conductors, parent);
}
case Element::Type: //1000
{
diff --git a/sources/ui/conductorpropertieseditorwidget.cpp b/sources/ui/conductorpropertieseditorwidget.cpp
index 8b50fa02d..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"
@@ -35,6 +35,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -44,11 +45,11 @@
/**
@brief ConductorPropertiesEditorWidget::ConductorPropertiesEditorWidget
- @param conductor : conductor to edit
+ @param conductors : conductors to edit
@param parent : parent widget
*/
ConductorPropertiesEditorWidget::ConductorPropertiesEditorWidget(
- Conductor *conductor, QWidget *parent) :
+ const QList &conductors, QWidget *parent) :
PropertiesEditorWidget(parent),
m_cpw(new ConductorPropertiesWidget(this))
{
@@ -76,12 +77,19 @@ ConductorPropertiesEditorWidget::ConductorPropertiesEditorWidget(
QStringLiteral("diagrameditor/conductor_apply_all"), on);
});
+ //Shown only when several conductors are selected: the fields hold
+ //the first one's values, and only the edited ones reach the others.
+ m_count_label = new QLabel(this);
+ m_count_label->setWordWrap(true);
+ m_count_label->hide();
+
auto *scroll = new QScrollArea(this);
scroll->setWidgetResizable(true);
scroll->setFrameShape(QFrame::NoFrame);
scroll->setWidget(m_cpw);
auto *layout = new QVBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
+ layout->addWidget(m_count_label);
layout->addWidget(m_apply_all_cb);
layout->addWidget(scroll);
setMinimumWidth(120);
@@ -100,7 +108,7 @@ ConductorPropertiesEditorWidget::ConductorPropertiesEditorWidget(
gb->installEventFilter(this);
setDisabled(true);
- setConductor(conductor);
+ setConductors(conductors);
}
/**
@@ -123,26 +131,64 @@ ConductorPropertiesEditorWidget::~ConductorPropertiesEditorWidget()
{}
/**
- @brief ConductorPropertiesEditorWidget::setConductor
- Set (or change) the conductor whose properties are edited.
- @param conductor
+ @brief ConductorPropertiesEditorWidget::setConductors
+ Set (or change) the conductors whose properties are edited.
+ @param conductors
*/
-void ConductorPropertiesEditorWidget::setConductor(Conductor *conductor)
+void ConductorPropertiesEditorWidget::setConductors(
+ const QList &conductors)
{
- if (!conductor) return;
- if (m_conductor && m_conductor != conductor)
- disconnect(m_conductor, &Conductor::propertiesChange,
- this, &ConductorPropertiesEditorWidget::updateUi);
- m_conductor = conductor;
- //Keep the dock in sync when the 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).
- connect(m_conductor, &Conductor::propertiesChange,
- this, &ConductorPropertiesEditorWidget::updateUi, Qt::UniqueConnection);
+ if (conductors.isEmpty()) return;
+
+ for (const QPointer &c : std::as_const(m_conductors))
+ if (c)
+ disconnect(c, &Conductor::propertiesChange,
+ this, &ConductorPropertiesEditorWidget::scheduleUpdateUi);
+ m_conductors.clear();
+
+ //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). One edit of N conductors emits N signals:
+ //they are gathered into one reload.
+ connect(c, &Conductor::propertiesChange,
+ this, &ConductorPropertiesEditorWidget::scheduleUpdateUi,
+ Qt::UniqueConnection);
+ }
+
+ const int count = conductors.size();
+ m_count_label->setText(
+ tr("%n conducteurs sélectionnés : seuls les champs modifiés leur sont appliqués.",
+ "selection properties panel", count));
+ m_count_label->setVisible(count > 1);
+
setEnabled(true);
updateUi();
}
+/**
+ @brief ConductorPropertiesEditorWidget::firstConductor
+ @return the first edited conductor still alive, its values are the ones
+ shown, or nullptr
+*/
+Conductor *ConductorPropertiesEditorWidget::firstConductor() const
+{
+ for (const QPointer &c : m_conductors)
+ if (c)
+ return c;
+ return nullptr;
+}
+
/**
@brief ConductorPropertiesEditorWidget::apply
Push the edit onto the diagram's undo stack.
@@ -153,10 +199,25 @@ void ConductorPropertiesEditorWidget::apply()
// programmatically (updateUi/reset): mid-load the widget holds a partial
// state that must not be committed onto the conductor.
if (m_updating) return;
- if (!m_conductor || !m_conductor->diagram()) return;
+ Conductor *first = firstConductor();
+ if (!first || !first->diagram()) return;
if (QUndoCommand *undo = associatedUndo())
- m_conductor->diagram()->undoStack().push(undo);
- m_shown = m_cpw->properties();
+ first->diagram()->undoStack().push(undo);
+ 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);
}
/**
@@ -242,7 +303,7 @@ void ConductorPropertiesEditorWidget::disconnectChangeSignals()
*/
void ConductorPropertiesEditorWidget::reset()
{
- if (!m_conductor) return;
+ if (!firstConductor()) return;
m_updating = true;
m_cpw->setProperties(m_shown);
m_updating = false;
@@ -250,13 +311,24 @@ void ConductorPropertiesEditorWidget::reset()
/**
@brief ConductorPropertiesEditorWidget::updateUi
- Reload the widget from the 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()
{
- if (!m_conductor) 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(m_conductor->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();
@@ -267,51 +339,31 @@ void ConductorPropertiesEditorWidget::updateUi()
@brief ConductorPropertiesEditorWidget::associatedUndo
@return the edit as one undo step, or nullptr if nothing changes.
- Only the fields the user changed are written: the conductor keeps every
- other value, including one the widget cannot show exactly.
+ Only the fields the user changed are applied, to each edited conductor:
+ with several conductors selected, each keeps its own text, function,
+ cable... unless that is the field being edited.
- When "apply to all" is ticked, every conductor on the same potential is
- updated too, in the same undo step (one undo reverts them all), as the
- modal dialog does (ConductorPropertiesDialog::PropertiesDialog).
+ When "apply to all" is ticked, every conductor on the same potential as
+ an edited one is updated too, in the same undo step (one undo reverts
+ them all), as the modal dialog does
+ (ConductorPropertiesDialog::PropertiesDialog).
*/
QUndoCommand *ConductorPropertiesEditorWidget::associatedUndo() const
{
- if (!m_conductor) 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 ConductorProperties new_properties = m_cpw->properties();
- if (new_properties == m_shown) return nullptr;
+ QList selected;
+ for (const QPointer &c : m_conductors)
+ if (c)
+ selected << c.data();
- QList targets {m_conductor};
- if (m_apply_all_cb && m_apply_all_cb->isChecked())
- for (Conductor *potential_conductor : m_conductor->relatedPotentialConductors())
- if (!targets.contains(potential_conductor))
- targets << potential_conductor;
-
- 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 a5f517738..ea915a706 100644
--- a/sources/ui/conductorpropertieseditorwidget.h
+++ b/sources/ui/conductorpropertieseditorwidget.h
@@ -23,18 +23,22 @@
#include
#include
+#include
class Conductor;
class ConductorPropertiesWidget;
class QCheckBox;
+class QLabel;
/**
@brief The ConductorPropertiesEditorWidget class
Hosts the existing ConductorPropertiesWidget in the dockable selection-
- properties panel, so a selected conductor can be edited in place like the
+ properties panel, so selected conductors can be edited in place like the
other item types, instead of only through the modal dialog (issue #500).
A pinned "apply to all conductors of the potential" checkbox (persisted)
mirrors the modal dialog's option to propagate edits to the whole potential.
+ When several conductors are selected, the panel shows the first one and
+ applies to each only the fields the user changed.
*/
class ConductorPropertiesEditorWidget : public PropertiesEditorWidget
{
@@ -42,10 +46,11 @@ class ConductorPropertiesEditorWidget : public PropertiesEditorWidget
public:
explicit ConductorPropertiesEditorWidget(
- Conductor *conductor = nullptr, QWidget *parent = nullptr);
+ const QList &conductors = {},
+ QWidget *parent = nullptr);
~ConductorPropertiesEditorWidget() override;
- void setConductor(Conductor *conductor);
+ void setConductors(const QList &conductors);
void apply() override;
void reset() override;
@@ -60,16 +65,20 @@ class ConductorPropertiesEditorWidget : public PropertiesEditorWidget
private:
void connectChangeSignals();
void disconnectChangeSignals();
+ void scheduleUpdateUi();
+ Conductor *firstConductor() const;
private:
ConductorPropertiesWidget *m_cpw = nullptr;
QCheckBox *m_apply_all_cb = nullptr;
- Conductor *m_conductor = nullptr;
+ QLabel *m_count_label = nullptr;
+ QList> m_conductors;
//What the widget showed before the edit: the fields that
//differ from it are the ones the user changed.
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 c355dcf3c..3b1085990 100644
--- a/tests/qttest/CMakeLists.txt
+++ b/tests/qttest/CMakeLists.txt
@@ -547,6 +547,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"