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 Conducteur Conductor + + + %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"