mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-03 01:04:13 +02:00
Merge pull request #1205 from ispyisail/feature/multi-wire-properties-panel
Edit several wires at once in the Selection properties panel
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -1055,6 +1055,15 @@ Note: these options DO NOT allow or block auto numberings, only their update pol
|
||||
<source>Conducteur</source>
|
||||
<translation>Conductor</translation>
|
||||
</message>
|
||||
<message numerus="yes">
|
||||
<location filename="../sources/ui/conductorpropertieseditorwidget.cpp" line="171"/>
|
||||
<source>%n conducteurs sélectionnés : seuls les champs modifiés leur sont appliqués.</source>
|
||||
<comment>selection properties panel</comment>
|
||||
<translation>
|
||||
<numerusform>%n conductor selected: only the fields you change are applied to it.</numerusform>
|
||||
<numerusform>%n conductors selected: only the fields you change are applied to them.</numerusform>
|
||||
</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>ConductorPropertiesWidget</name>
|
||||
@@ -1302,6 +1311,17 @@ Note: these options DO NOT allow or block auto numberings, only their update pol
|
||||
<source>Style :</source>
|
||||
<translation>Style:</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/ui/conductorpropertieswidget.cpp" line="216"/>
|
||||
<source>Plusieurs valeurs</source>
|
||||
<comment>several conductors, different values</comment>
|
||||
<translation>Several values</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/ui/conductorpropertieswidget.cpp" line="231"/>
|
||||
<source>Plusieurs conducteurs sélectionnés : le texte se modifie sur un seul conducteur à la fois.</source>
|
||||
<translation>Several conductors selected: the text is changed on one conductor at a time.</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>ConfigSaveLoaderWidget</name>
|
||||
|
||||
@@ -1054,6 +1054,15 @@ Remarque: Ces options n'autorisent ou bloquent l'auto numérotation, s
|
||||
<source>Conducteur</source>
|
||||
<translation></translation>
|
||||
</message>
|
||||
<message numerus="yes">
|
||||
<location filename="../sources/ui/conductorpropertieseditorwidget.cpp" line="171"/>
|
||||
<source>%n conducteurs sélectionnés : seuls les champs modifiés leur sont appliqués.</source>
|
||||
<comment>selection properties panel</comment>
|
||||
<translation>
|
||||
<numerusform>%n conducteur sélectionné : seuls les champs modifiés lui sont appliqués.</numerusform>
|
||||
<numerusform>%n conducteurs sélectionnés : seuls les champs modifiés leur sont appliqués.</numerusform>
|
||||
</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>ConductorPropertiesWidget</name>
|
||||
@@ -1301,6 +1310,17 @@ Remarque: Ces options n'autorisent ou bloquent l'auto numérotation, s
|
||||
<comment>conductor style: dashed and dotted line</comment>
|
||||
<translation></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/ui/conductorpropertieswidget.cpp" line="216"/>
|
||||
<source>Plusieurs valeurs</source>
|
||||
<comment>several conductors, different values</comment>
|
||||
<translation></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/ui/conductorpropertieswidget.cpp" line="231"/>
|
||||
<source>Plusieurs conducteurs sélectionnés : le texte se modifie sur un seul conducteur à la fois.</source>
|
||||
<translation></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>ConfigSaveLoaderWidget</name>
|
||||
|
||||
@@ -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
|
||||
@@ -38,6 +38,7 @@
|
||||
#include "../ui/shapegraphicsitempropertieswidget.h"
|
||||
|
||||
#include <QGraphicsItem>
|
||||
#include <QSet>
|
||||
#include <QSettings>
|
||||
|
||||
/**
|
||||
@@ -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<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 (!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<ConductorTextItem *>(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<Conductor *> conductors;
|
||||
for (QGraphicsItem *qgi : std::as_const(items)) {
|
||||
conductors << static_cast<Conductor *>(qgi);
|
||||
}
|
||||
auto conductor = static_cast<Conductor*>(item);
|
||||
|
||||
if (class_name == ConductorPropertiesEditorWidget::staticMetaObject.className())
|
||||
{
|
||||
static_cast<ConductorPropertiesEditorWidget*>(editor)->setConductor(conductor);
|
||||
static_cast<ConductorPropertiesEditorWidget*>(editor)->setConductors(conductors);
|
||||
return editor;
|
||||
}
|
||||
return new ConductorPropertiesEditorWidget(conductor, parent);
|
||||
return new ConductorPropertiesEditorWidget(conductors, parent);
|
||||
}
|
||||
case Element::Type: //1000
|
||||
{
|
||||
|
||||
@@ -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 <QComboBox>
|
||||
#include <QGroupBox>
|
||||
#include <QKeyEvent>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QScrollArea>
|
||||
#include <QSettings>
|
||||
@@ -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<Conductor *> &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<Conductor *> &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<Conductor> &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<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). 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<Conductor> &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<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(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<Conductor *> selected;
|
||||
for (const QPointer<Conductor> &c : m_conductors)
|
||||
if (c)
|
||||
selected << c.data();
|
||||
|
||||
QList<Conductor *> 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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -23,18 +23,22 @@
|
||||
|
||||
#include <QList>
|
||||
#include <QMetaObject>
|
||||
#include <QPointer>
|
||||
|
||||
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<Conductor *> &conductors = {},
|
||||
QWidget *parent = nullptr);
|
||||
~ConductorPropertiesEditorWidget() override;
|
||||
|
||||
void setConductor(Conductor *conductor);
|
||||
void setConductors(const QList<Conductor *> &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<QPointer<Conductor>> 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<QMetaObject::Connection> m_live_connections;
|
||||
bool m_updating = false;
|
||||
bool m_update_pending = false;
|
||||
};
|
||||
|
||||
#endif // CONDUCTORPROPERTIESEDITORWIDGET_H
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user