Add editing the information of several elements at once (#1439)

Selecting several elements left the Selection properties panel blank and
Edit (Ctrl+E) disabled, so giving many elements the same supplier or
manufacturer meant editing them one by one.

The panel now shows the Information fields for two or more elements, or
for elements selected with other items (left unchanged, and counted in
the panel's header). Ctrl+E opens the same editor as a dialog when only
elements are selected.

- A field shows the value every element shares, or "Multiple values".
  Left alone, each element keeps its own value; typed text goes to every
  element; the erase box clears the field on every element.
- A restore button per field, and Reset, bring back the values the
  elements had.
- One apply is one undo step. The keystrokes typed in the panel merge
  into one step but never into an earlier change of the same elements
  (a paste of element information, another edit).
- Numbering scheme, label freezing and the number picker are left out:
  they are about one element. Custom properties are kept as they are.

Also:
- An element's own texts, selected with it by Select all, count as part
  of the element, not as other items.
- ElementInfoWidget::reset(): Reset in the element dialog now brings back
  the Information tab's values; it did nothing.
- The panel opens an element's editor on the tab picked last, instead of
  the first tab, after the selection went to something else and back.

Test: tst_multielementinfo.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ispyisail
2026-10-18 21:44:00 +13:00
parent e46bb97c5b
commit ce0798bb18
12 changed files with 1506 additions and 48 deletions
+2
View File
@@ -829,6 +829,8 @@ set(QET_SRC_FILES
${QET_DIR}/sources/ui/elementinfopartwidget.h
${QET_DIR}/sources/ui/elementinfowidget.cpp
${QET_DIR}/sources/ui/elementinfowidget.h
${QET_DIR}/sources/ui/multielementinfowidget.cpp
${QET_DIR}/sources/ui/multielementinfowidget.h
${QET_DIR}/sources/ui/terminalnumberingdialog.cpp
${QET_DIR}/sources/ui/terminalnumberingdialog.h
${QET_DIR}/sources/ui/cablenumberingdialog.cpp
+28 -2
View File
@@ -39,7 +39,9 @@
#include "qeticons.h"
#include "qetpalette.h"
#include "titleblock/integrationmovetemplateshandler.h"
#include "PropertiesEditor/propertieseditordialog.h"
#include "ui/diagrampropertiesdialog.h"
#include "ui/multielementinfowidget.h"
#include "ui/multipastedialog.h"
#include "undocommand/changetitleblockcommand.h"
#include "utils/conductorcreator.h"
@@ -2080,9 +2082,33 @@ bool DiagramView::selectedItemHasFocus()
*/
void DiagramView::editSelection()
{
if (m_diagram -> isReadOnly() || m_diagram -> selectedItems().size() != 1 ) return;
if (m_diagram -> isReadOnly()) return;
QGraphicsItem *item = m_diagram->selectedItems().first();
//Several elements and nothing else: edit their information together (#1439)
const QList<QGraphicsItem *> selection =
MultiElementInfoWidget::withoutOwnTexts(m_diagram -> selectedItems());
if (selection.size() > 1)
{
bool only_elements = true;
for (QGraphicsItem *qgi : selection) {
if (qgi->type() != Element::Type) {
only_elements = false;
break;
}
}
const QList<Element *> elements = MultiElementInfoWidget::informationElements(selection);
if (only_elements && elements.size() > 1)
{
PropertiesEditorDialog dialog(
new MultiElementInfoWidget(elements, selection.size() - elements.size()),
this);
dialog.exec();
}
return;
}
if (selection.size() != 1) return;
QGraphicsItem *item = selection.first();
//We use dynamic_cast instead of qgraphicsitem_cast for QetGraphicsItem
//because they haven't got their own type().
+28 -12
View File
@@ -39,6 +39,7 @@
#include "../ui/conductorpropertieseditorwidget.h"
#include "../ui/imagepropertieswidget.h"
#include "../ui/inditextpropertieswidget.h"
#include "../ui/multielementinfowidget.h"
#include "../ui/shapegraphicsitempropertieswidget.h"
#include <QGraphicsItem>
@@ -115,7 +116,7 @@ PropertiesEditorWidget *PropertiesEditorFactory::propertiesEditor(
mapped << qgi;
}
}
items = mapped;
items = MultiElementInfoWidget::withoutOwnTexts(mapped);
const int count_ = items.size();
if (count_ == 0) {
@@ -124,19 +125,37 @@ PropertiesEditorWidget *PropertiesEditorFactory::propertiesEditor(
QGraphicsItem *item = items.first();
const int type_ = item->type();
//The editor widget can only edit one item
//or several items of the same type
for (auto qgi : items) {
if (qgi->type() != type_) {
return nullptr;
}
}
QString class_name;
if (editor) {
class_name = editor->metaObject()->className();
}
//The editor widget can only edit one item
//or several items of the same type,
//or the information of several elements (#1439)
bool same_type = true;
for (auto qgi : items) {
if (qgi->type() != type_) {
same_type = false;
break;
}
}
if (!same_type || (type_ == Element::Type && count_ > 1))
{
const QList<Element *> elements =
MultiElementInfoWidget::informationElements(items);
if (elements.isEmpty()) {
return nullptr;
}
const int other_items = count_ - elements.size();
if (class_name == MultiElementInfoWidget::staticMetaObject.className())
{
static_cast<MultiElementInfoWidget *>(editor)->setElements(elements, other_items);
return editor;
}
return new MultiElementInfoWidget(elements, other_items, parent);
}
switch (type_)
{
case Conductor::Type: //1001
@@ -162,9 +181,6 @@ PropertiesEditorWidget *PropertiesEditorFactory::propertiesEditor(
}
case Element::Type: //1000
{
if (count_ > 1) {
return nullptr;
}
auto elmt = static_cast<Element*>(item);
//auto created_editor = new ElementPropertiesWidget(elmt, parent);
+26 -34
View File
@@ -64,6 +64,7 @@
#include "factory/qetgraphicstablefactory.h"
#include "print/projectprintwindow.h"
#include "project/projectpropertieshandler.h"
#include "ui/multielementinfowidget.h"
#include "ui/projectpropertiesdialog.h"
#include "projectview.h"
#include "qetproject.h"
@@ -3085,6 +3086,21 @@ void QETDiagramEditor::slot_updateUndoStack()
undo_group.setActiveStack(currentProjectView()->project()->undoStack());
}
namespace {
/**
@return the elements of @p diagram's selection which take information
*/
QList<Element *> selectedInformationElements(Diagram *diagram)
{
QList<Element *> list;
for (Element *element : DiagramContent(diagram).m_elements) {
if (MultiElementInfoWidget::takesInformations(element))
list << element;
}
return list;
}
} // namespace
/**
@brief QETDiagramEditor::slot_updateComplexActions
Manage the actions that need some conditions to be enabled or not.
@@ -3211,7 +3227,16 @@ void QETDiagramEditor::slot_updateComplexActions()
+ selected_conductors_count
+ selected_cables;
if (selected_editable == 1)
//Several elements and nothing else: edit their information together (#1439)
if (!ro && selected_editable > 1 && selected_editable == selected_elements_count
&& selectedInformationElements(diagram_).size() > 1)
{
m_edit_selection -> setEnabled(true);
m_edit_selection -> setText(tr("Edit the information of the elements",
"edit several elements"));
m_edit_selection -> setIcon(QET::Icons::ElementEdit);
}
else if (selected_editable == 1)
{
m_edit_selection -> setEnabled(true);
//edit element
@@ -3323,39 +3348,6 @@ void QETDiagramEditor::slot_updateModeActions()
}
}
namespace {
/**
@return true if @p element has information the user can edit, the
same element kinds ElementPropertiesWidget gives an ElementInfoWidget.
Plain slaves and folio reports take theirs from what they are linked to.
*/
bool takesElementInformations(const Element *element)
{
switch (element->linkType()) {
case Element::Simple:
case Element::Thumbnail:
case Element::Master:
case Element::Terminale:
return true;
default:
return false;
}
}
/**
@return the elements of @p diagram's selection which take information
*/
QList<Element *> selectedInformationElements(Diagram *diagram)
{
QList<Element *> list;
for (Element *element : DiagramContent(diagram).m_elements) {
if (takesElementInformations(element))
list << element;
}
return list;
}
} // namespace
/**
@brief QETDiagramEditor::slot_updatePasteAction
Gere les actions ayant besoin du presse-papier
+12
View File
@@ -111,6 +111,18 @@ void ElementInfoWidget::apply()
m_element -> diagram() -> undoStack().push(undo);
}
/**
@brief ElementInfoWidget::reset
Forget the edits not applied yet: show again the information
the element has.
*/
void ElementInfoWidget::reset()
{
if (m_element) {
updateUi();
}
}
/**
@brief ElementInfoWidget::associatedUndo
If the edited info is different of the actual element info,
+1
View File
@@ -53,6 +53,7 @@ class ElementInfoWidget : public AbstractElementPropertiesEditorWidget
void setElement(Element *element) override;
void apply() override;
void reset() override;
QUndoCommand *associatedUndo () const override;
QString title() const override {return tr("Information");}
bool setLiveEdit(bool live_edit) override;
+27
View File
@@ -36,6 +36,11 @@
#include <QUndoStack>
#include <QVBoxLayout>
namespace {
//The tab last picked in the selection properties panel
QString s_live_tab;
}
/**
@brief ElementPropertiesWidget::ElementPropertiesWidget
Default constructor
@@ -149,6 +154,7 @@ void ElementPropertiesWidget::setDynamicText(DynamicElementTextItem *text)
{
DynamicElementTextItemEditor *detie = static_cast<DynamicElementTextItemEditor *>(aepew);
m_tab->setCurrentWidget(detie);
m_text_tab_wanted = true;
detie->setCurrentText(text);
}
}
@@ -175,6 +181,7 @@ void ElementPropertiesWidget::setTextsGroup(ElementTextItemGroup *group)
{
DynamicElementTextItemEditor *detie = static_cast<DynamicElementTextItemEditor *>(aepew);
m_tab->setCurrentWidget(detie);
m_text_tab_wanted = true;
detie->setCurrentGroup(group);
}
}
@@ -225,6 +232,18 @@ bool ElementPropertiesWidget::setLiveEdit(bool live_edit)
foreach (AbstractElementPropertiesEditorWidget *aepew, m_list_editor)
aepew->setLiveEdit(m_live_edit);
if (m_live_edit && !m_text_tab_wanted && !s_live_tab.isEmpty())
{
for (int i = 0 ; i < m_tab->count() ; ++i)
{
if (m_tab->tabText(i) == s_live_tab)
{
m_tab->setCurrentIndex(i);
break;
}
}
}
return true;
}
@@ -262,6 +281,14 @@ void ElementPropertiesWidget::editElement()
void ElementPropertiesWidget::buildGui()
{
m_tab = new QTabWidget(this);
//The selection properties panel builds a new editor each time
//the selection goes from one element to something else and back:
//it opens on the tab last picked there (#1439)
connect(m_tab, &QTabWidget::tabBarClicked, this, [this](int index) {
if (m_live_edit && index >= 0) {
s_live_tab = m_tab->tabText(index);
}
});
QVBoxLayout *main_layout = new QVBoxLayout(this);
main_layout -> addWidget(m_tab);
setLayout(main_layout);
+1
View File
@@ -61,6 +61,7 @@ class ElementPropertiesWidget : public AbstractElementPropertiesEditorWidget
QTabWidget *m_tab;
QList <AbstractElementPropertiesEditorWidget *> m_list_editor;
QWidget *m_general_widget;
bool m_text_tab_wanted = false;
};
#endif // ELEMENTPROPERTIESWIDGET_H
+837
View File
@@ -0,0 +1,837 @@
/*
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/>.
*/
#include "multielementinfowidget.h"
#include "../dataBase/projectdatabase.h"
#include "../diagram.h"
#include "../qet.h"
#include "../qeticons.h"
#include "../qetgraphicsitem/dynamicelementtextitem.h"
#include "../qetgraphicsitem/element.h"
#include "../qetgraphicsitem/elementtextitemgroup.h"
#include "../qetinformation.h"
#include "../qetproject.h"
#include "../undocommand/changeelementinformationcommand.h"
#include <QCheckBox>
#include <QCompleter>
#include <QGridLayout>
#include <QLabel>
#include <QLineEdit>
#include <QScrollArea>
#include <QSet>
#include <QSqlQuery>
#include <QTimer>
#include <QToolButton>
#include <QVBoxLayout>
namespace {
/**
The change of one editing session: the keystrokes applied one by one in
the selection panel make one undo step, but never join a change made
before, by the same editor or another one, to the same elements.
*/
class MultiElementInfoCommand : public ChangeElementInformationCommand
{
public:
MultiElementInfoCommand(
const QMap<QPointer<Element>, QPair<DiagramContext, DiagramContext>> &map,
quint64 session) :
ChangeElementInformationCommand(map),
m_session(session)
{}
int id() const override {return 1439;}
bool mergeWith(const QUndoCommand *other) override
{
if (other->id() != id()
|| static_cast<const MultiElementInfoCommand *>(other)->m_session != m_session) {
return false;
}
return ChangeElementInformationCommand::mergeWith(other);
}
private:
quint64 m_session;
};
quint64 s_last_session = 0;
const QString EXCLUDE_FROM_BOM = QStringLiteral("exclude_from_bom");
const QString AUTO_NUM_LOCKED = QStringLiteral("auto_num_locked");
const QString POTENTIAL_ISOLATING = QStringLiteral("potential_isolating");
QString flagText(bool on)
{
return on ? QStringLiteral("true") : QStringLiteral("false");
}
}
/**
@brief MultiElementInfoWidget::MultiElementInfoWidget
@param elements : the elements to edit
@param other_items : how many other selected items this editor leaves
alone, only shown to the user
@param parent
*/
MultiElementInfoWidget::MultiElementInfoWidget(const QList<Element *> &elements,
int other_items,
QWidget *parent) :
PropertiesEditorWidget(parent)
{
buildInterface();
setElements(elements, other_items);
}
MultiElementInfoWidget::~MultiElementInfoWidget()
{
qDeleteAll(m_rows);
qDeleteAll(m_flags);
}
/**
@brief MultiElementInfoWidget::takesInformations
@return true if @p element has information the user can edit: the
element kinds ElementPropertiesWidget gives an Information tab.
Plain slaves and folio reports take theirs from what they are linked to.
*/
bool MultiElementInfoWidget::takesInformations(const Element *element)
{
if (!element) {
return false;
}
switch (element->linkType()) {
case Element::Simple:
case Element::Thumbnail:
case Element::Master:
case Element::Terminale:
return true;
default:
return false;
}
}
/**
@brief MultiElementInfoWidget::informationElements
@return the elements of @p items which take information
*/
QList<Element *> MultiElementInfoWidget::informationElements(const QList<QGraphicsItem *> &items)
{
QList<Element *> list;
for (QGraphicsItem *qgi : items) {
if (qgi->type() != Element::Type) {
continue;
}
auto element = static_cast<Element *>(qgi);
if (takesInformations(element)) {
list << element;
}
}
return list;
}
/**
@brief MultiElementInfoWidget::withoutOwnTexts
@return @p items without the texts and text groups of elements which
are in @p items too: select all takes an element's texts with it, and
they are part of the element rather than other items.
*/
QList<QGraphicsItem *> MultiElementInfoWidget::withoutOwnTexts(const QList<QGraphicsItem *> &items)
{
QSet<QGraphicsItem *> elements;
for (QGraphicsItem *qgi : items) {
if (qgi->type() == Element::Type) {
elements.insert(qgi);
}
}
if (elements.isEmpty()) {
return items;
}
QList<QGraphicsItem *> list;
for (QGraphicsItem *qgi : items)
{
Element *owner = nullptr;
if (qgi->type() == DynamicElementTextItem::Type) {
owner = static_cast<DynamicElementTextItem *>(qgi)->parentElement();
} else if (auto group = dynamic_cast<ElementTextItemGroup *>(qgi)) {
owner = group->parentElement();
}
if (!owner || !elements.contains(owner)) {
list << qgi;
}
}
return list;
}
/**
@brief MultiElementInfoWidget::setElements
Edit @p elements, forgetting any pending change.
*/
void MultiElementInfoWidget::setElements(const QList<Element *> &elements, int other_items)
{
for (const auto &element : std::as_const(m_elements)) {
if (element) {
disconnect(element.data(), &Element::elementInfoChange,
this, &MultiElementInfoWidget::elementInfoChanged);
}
}
m_elements.clear();
for (Element *element : elements) {
m_elements << QPointer<Element>(element);
connect(element, &Element::elementInfoChange,
this, &MultiElementInfoWidget::elementInfoChanged);
}
m_other_items = other_items;
updateUi();
}
/**
@brief MultiElementInfoWidget::elements
@return the edited elements still alive
*/
QList<Element *> MultiElementInfoWidget::elements() const
{
QList<Element *> list;
for (const auto &element : m_elements) {
if (element) {
list << element.data();
}
}
return list;
}
/**
@brief MultiElementInfoWidget::apply
Push the change of every element, as one undo step.
*/
void MultiElementInfoWidget::apply()
{
QUndoCommand *undo = associatedUndo();
if (!undo) {
return;
}
const auto list = elements();
if (list.isEmpty() || !list.first()->diagram()) {
delete undo;
return;
}
m_applying = true;
list.first()->diagram()->undoStack().push(undo);
m_applying = false;
}
/**
@brief MultiElementInfoWidget::associatedUndo
@return the command giving each element its new information, or
nullptr if no element would change.
A field left alone gives each element back the value it had when the
editor was filled, so restoring a field in live edit undoes it.
*/
QUndoCommand *MultiElementInfoWidget::associatedUndo() const
{
QMap<QPointer<Element>, QPair<DiagramContext, DiagramContext>> changes;
for (Element *element : elements())
{
const DiagramContext old_info = element->elementInformations();
const DiagramContext snap = m_snapshot.value(element, old_info);
DiagramContext new_info = old_info;
for (const Row *row : m_rows)
{
QString txt;
switch (row->state) {
case Keep:
if (snap.contains(row->key)) {
new_info.addValue(row->key, snap.value(row->key),
snap.keyMustShow(row->key));
} else {
new_info.remove(row->key);
}
continue;
case Clear:
new_info.remove(row->key);
continue;
case Set:
if (!row->edit->hasAcceptableInput()) {
continue;
}
txt = row->edit->text();
txt.remove(QStringLiteral("\r"));
txt.remove(QStringLiteral("\n"));
if (txt.isEmpty()) {
new_info.remove(row->key);
} else {
new_info.addValue(row->key, txt,
old_info.contains(row->key)
? old_info.keyMustShow(row->key)
: true);
}
continue;
}
}
//The element keeps following its numbering scheme as long as
//its formula is left as it was; a formula given by hand is its own.
if (new_info.value(QETInformation::ELMT_FORMULA)
== snap.value(QETInformation::ELMT_FORMULA)
&& snap.contains(QETInformation::ELMT_FORMULA_ID)) {
new_info.addValue(QETInformation::ELMT_FORMULA_ID,
snap.value(QETInformation::ELMT_FORMULA_ID), false);
} else {
new_info.remove(QETInformation::ELMT_FORMULA_ID);
}
for (const Flag *flag : m_flags)
{
if (flag->check->isHidden()) {
continue;
}
if (!flag->changed) {
if (snap.contains(flag->key)) {
new_info.addValue(flag->key, snap.value(flag->key),
snap.keyMustShow(flag->key));
} else {
new_info.remove(flag->key);
}
} else {
new_info.addValue(flag->key, flagText(flag->check->isChecked()));
}
}
if (new_info != old_info) {
changes.insert(QPointer<Element>(element), qMakePair(old_info, new_info));
}
}
if (changes.isEmpty()) {
return nullptr;
}
return new MultiElementInfoCommand(changes, m_session);
}
/**
@brief MultiElementInfoWidget::reset
Bring back every field to what the elements had, and in live edit
give it back to the elements.
*/
void MultiElementInfoWidget::reset()
{
m_updating = true;
for (Row *row : std::as_const(m_rows)) {
restoreRow(row);
}
for (Flag *flag : std::as_const(m_flags)) {
restoreFlag(flag);
}
m_updating = false;
updateLabelRow();
changed();
}
/**
@brief MultiElementInfoWidget::updateUi
Read the elements again, forgetting any pending change.
*/
void MultiElementInfoWidget::updateUi()
{
takeSnapshot();
fillRows();
updateHeader();
updateSuggestions();
}
/**
@brief MultiElementInfoWidget::fieldState
@return what the field @p key does on apply
*/
MultiElementInfoWidget::FieldState MultiElementInfoWidget::fieldState(const QString &key) const
{
const Row *row = rowForKey(key);
return row ? row->state : Keep;
}
/**
@brief MultiElementInfoWidget::setFieldText
Type @p text in the field @p key, as the user would.
*/
void MultiElementInfoWidget::setFieldText(const QString &key, const QString &text)
{
if (Row *row = rowForKey(key)) {
row->edit->setText(text);
}
}
/**
@brief MultiElementInfoWidget::setFieldCleared
Tick or untick the erase box of the field @p key, as the user would.
*/
void MultiElementInfoWidget::setFieldCleared(const QString &key, bool cleared)
{
if (Row *row = rowForKey(key)) {
row->erase->setChecked(cleared);
rowErased(row);
}
}
/**
@brief MultiElementInfoWidget::restoreField
Click the restore button of the field @p key.
*/
void MultiElementInfoWidget::restoreField(const QString &key)
{
if (Row *row = rowForKey(key)) {
restoreRow(row);
changed();
}
}
/**
@brief MultiElementInfoWidget::fieldIsMixed
@return true if the elements had different values for @p key
*/
bool MultiElementInfoWidget::fieldIsMixed(const QString &key) const
{
const Row *row = rowForKey(key);
return row && row->mixed;
}
/**
@brief MultiElementInfoWidget::setLiveEdit
@param live_edit : true to apply every change at once
@return always true
*/
bool MultiElementInfoWidget::setLiveEdit(bool live_edit)
{
m_live_edit = live_edit;
return true;
}
/**
@brief MultiElementInfoWidget::buildInterface
One row per information key: name, text, erase box, restore button.
The numbering scheme, label freezing and number picker of the
one-element editor are left out: they are about one element.
*/
void MultiElementInfoWidget::buildInterface()
{
auto main_layout = new QVBoxLayout(this);
main_layout->setContentsMargins(0, 0, 0, 0);
m_header = new QLabel(this);
m_header->setWordWrap(true);
m_header->setContentsMargins(5, 5, 5, 0);
main_layout->addWidget(m_header);
auto flags_layout = new QVBoxLayout();
main_layout->addLayout(flags_layout);
const QList<QPair<QString, QString>> flags {
{EXCLUDE_FROM_BOM, tr("Exclude from the bill of materials")},
{POTENTIAL_ISOLATING, tr("Function separation")},
{AUTO_NUM_LOCKED, tr("Exclude from automatic numbering")}
};
for (const auto &pair : flags)
{
auto flag = new Flag;
flag->key = pair.first;
flag->check = new QCheckBox(pair.second, this);
flag->restore = new QToolButton(this);
flag->restore->setIcon(QET::Icons::EditUndo);
flag->restore->setAutoRaise(true);
flag->restore->setToolTip(tr("Restore the values the elements had"));
auto row_layout = new QHBoxLayout();
row_layout->setContentsMargins(5, 0, 5, 0);
row_layout->addWidget(flag->check, 1);
row_layout->addWidget(flag->restore);
flags_layout->addLayout(row_layout);
m_flags << flag;
connect(flag->check, &QCheckBox::clicked, this, [this, flag]() {
flagClicked(flag);
});
connect(flag->restore, &QToolButton::clicked, this, [this, flag]() {
restoreFlag(flag);
changed();
});
}
auto scroll = new QScrollArea(this);
scroll->setWidgetResizable(true);
scroll->setFrameShape(QFrame::NoFrame);
auto content = new QWidget(scroll);
auto grid = new QGridLayout(content);
grid->setColumnStretch(1, 1);
QStringList keys = QETInformation::elementInfoKeys();
//It has its own check box above, see ElementInfoWidget::buildInterface
keys.removeAll(EXCLUDE_FROM_BOM);
int line = 0;
for (const QString &key : std::as_const(keys))
{
auto row = new Row;
row->key = key;
row->label = new QLabel(QETInformation::translatedInfoKey(key), content);
row->edit = new QLineEdit(content);
row->edit->setClearButtonEnabled(true);
row->erase = new QCheckBox(content);
row->erase->setToolTip(tr("Delete this text"));
row->restore = new QToolButton(content);
row->restore->setIcon(QET::Icons::EditUndo);
row->restore->setAutoRaise(true);
row->restore->setToolTip(tr("Restore the values the elements had"));
if (key == QETInformation::ELMT_WIDTH
|| key == QETInformation::ELMT_HEIGHT
|| key == QETInformation::ELMT_DEPTH) {
row->edit->setValidator(new QETInformation::NumericInfoValidator(row->edit));
row->edit->setToolTip(tr("Decimal number with a dot as separator (e.g. 80.5)"));
}
grid->addWidget(row->label, line, 0);
grid->addWidget(row->edit, line, 1);
grid->addWidget(row->erase, line, 2);
grid->addWidget(row->restore, line, 3);
++line;
m_rows << row;
connect(row->edit, &QLineEdit::textChanged, this, [this, row]() {
if (!m_updating) {
rowEdited(row);
}
});
connect(row->erase, &QCheckBox::clicked, this, [this, row]() {
rowErased(row);
});
connect(row->restore, &QToolButton::clicked, this, [this, row]() {
restoreRow(row);
changed();
});
}
grid->setRowStretch(line, 1);
scroll->setWidget(content);
main_layout->addWidget(scroll, 1);
}
/**
@brief MultiElementInfoWidget::takeSnapshot
Keep the information every element has now: the values a field left
alone gives back.
*/
void MultiElementInfoWidget::takeSnapshot()
{
m_session = ++s_last_session;
m_snapshot.clear();
for (Element *element : elements()) {
m_snapshot.insert(element, element->elementInformations());
}
}
/**
@brief MultiElementInfoWidget::fillRows
Show in each field the value shared by every element, or that the
values differ.
*/
void MultiElementInfoWidget::fillRows()
{
const auto list = elements();
m_updating = true;
for (Row *row : std::as_const(m_rows))
{
row->mixed = false;
row->initial.clear();
bool first = true;
for (Element *element : list) {
const QString value = m_snapshot.value(element).value(row->key).toString();
if (first) {
row->initial = value;
first = false;
} else if (value != row->initial) {
row->mixed = true;
row->initial.clear();
break;
}
}
restoreRow(row);
}
bool all_terminals = !list.isEmpty();
for (Element *element : list) {
if (element->elementData().m_type != ElementData::Terminal) {
all_terminals = false;
break;
}
}
for (Flag *flag : std::as_const(m_flags))
{
flag->check->setVisible(flag->key == EXCLUDE_FROM_BOM || all_terminals);
flag->restore->setVisible(flag->check->isVisible());
int on = 0;
for (Element *element : list) {
if (QET::infoFlagIsTrue(m_snapshot.value(element).value(flag->key).toString())) {
++on;
}
}
flag->initial = on == 0 ? Qt::Unchecked
: on == list.size() ? Qt::Checked
: Qt::PartiallyChecked;
restoreFlag(flag);
}
m_updating = false;
updateLabelRow();
}
/**
@brief MultiElementInfoWidget::updateHeader
Say how many elements are edited, and that the rest of the selection
is not.
*/
void MultiElementInfoWidget::updateHeader()
{
QString text = tr("Elements edited: %1").arg(elements().size());
if (m_other_items > 0) {
text += QStringLiteral("\n")
+ tr("Other selected items, left unchanged: %1").arg(m_other_items);
}
m_header->setText(text);
}
/**
@brief MultiElementInfoWidget::updateSuggestions
Offer, while typing, the values the project already uses,
the way ElementInfoWidget::updateSuggestions does.
*/
void MultiElementInfoWidget::updateSuggestions()
{
const auto list = elements();
Diagram *diagram = list.isEmpty() ? nullptr : list.first()->diagram();
QETProject *project = diagram ? diagram->project() : nullptr;
if (!project || !project->dataBase()) {
return;
}
//Only a column of element_info can be queried; the key becomes
//part of the SQL text, so it is checked against that list.
const QStringList columns = QETInformation::elementInfoKeys();
for (Row *row : std::as_const(m_rows))
{
if (row->key == QETInformation::ELMT_LABEL || !columns.contains(row->key)) {
continue;
}
QStringList values;
auto query = project->dataBase()->newQuery(QStringLiteral(
"SELECT v, MAX(n) FROM ("
"SELECT \"%1\" AS v, COUNT(*) AS n FROM element_info "
"WHERE \"%1\" IS NOT NULL AND \"%1\" != '' "
"GROUP BY \"%1\") "
"GROUP BY v COLLATE NOCASE "
"ORDER BY v COLLATE NOCASE").arg(row->key));
while (query.next()) {
values << query.value(0).toString();
}
if (values.isEmpty()) {
row->edit->setCompleter(nullptr);
continue;
}
auto completer = new QCompleter(values, row->edit);
completer->setCaseSensitivity(Qt::CaseInsensitive);
completer->setFilterMode(Qt::MatchContains);
row->edit->setCompleter(completer);
}
}
/**
@brief MultiElementInfoWidget::rowEdited
The user typed in a field: its text goes to every element, unless
it is back to what they all had.
*/
void MultiElementInfoWidget::rowEdited(Row *row)
{
const QString text = row->edit->text();
if (row->mixed ? text.isEmpty() : text == row->initial) {
row->state = Keep;
} else {
row->state = Set;
}
updateRowLook(row);
if (row->key == QETInformation::ELMT_FORMULA) {
updateLabelRow();
}
changed();
}
/**
@brief MultiElementInfoWidget::rowErased
The erase box of a field was ticked or unticked.
*/
void MultiElementInfoWidget::rowErased(Row *row)
{
m_updating = true;
if (row->erase->isChecked()) {
row->state = Clear;
row->edit->clear();
} else {
row->state = Keep;
row->edit->setText(row->initial);
}
m_updating = false;
updateRowLook(row);
if (row->key == QETInformation::ELMT_FORMULA) {
updateLabelRow();
}
changed();
}
/**
@brief MultiElementInfoWidget::restoreRow
Bring a field back to what the elements had. Does not apply.
*/
void MultiElementInfoWidget::restoreRow(Row *row)
{
const bool was_updating = m_updating;
m_updating = true;
row->state = Keep;
row->erase->setChecked(false);
row->edit->setText(row->initial);
m_updating = was_updating;
updateRowLook(row);
if (!m_updating && row->key == QETInformation::ELMT_FORMULA) {
updateLabelRow();
}
}
/**
@brief MultiElementInfoWidget::restoreFlag
Bring a check box back to what the elements had. Does not apply.
*/
void MultiElementInfoWidget::restoreFlag(Flag *flag)
{
flag->changed = false;
flag->check->setTristate(flag->initial == Qt::PartiallyChecked);
flag->check->setCheckState(flag->initial);
flag->restore->setEnabled(false);
}
/**
@brief MultiElementInfoWidget::flagClicked
A click gives the box's state to every element. A box which started
"partly checked" goes on as a plain on/off box: cycling back through
"partly" is what its restore button is for.
*/
void MultiElementInfoWidget::flagClicked(Flag *flag)
{
if (flag->check->isTristate()) {
flag->check->setTristate(false);
if (flag->check->checkState() == Qt::PartiallyChecked) {
flag->check->setCheckState(Qt::Checked);
}
}
flag->changed = flag->check->checkState() != flag->initial;
flag->restore->setEnabled(flag->changed);
changed();
}
/**
@brief MultiElementInfoWidget::updateRowLook
A field whose values differ says so; a field which will change
something has its restore button on and its name in bold.
*/
void MultiElementInfoWidget::updateRowLook(Row *row)
{
row->edit->setDisabled(row->state == Clear);
if (row->state == Clear) {
row->edit->setPlaceholderText(tr("Delete this text"));
} else if (row->mixed) {
row->edit->setPlaceholderText(tr("Multiple values"));
} else {
row->edit->setPlaceholderText(QString());
}
row->restore->setEnabled(row->state != Keep);
QFont font = row->label->font();
font.setBold(row->state != Keep);
row->label->setFont(font);
}
/**
@brief MultiElementInfoWidget::updateLabelRow
As in the one-element editor, the label cannot be typed while the
formula field holds a formula: the formula makes the label.
*/
void MultiElementInfoWidget::updateLabelRow()
{
Row *formula = rowForKey(QETInformation::ELMT_FORMULA);
Row *label = rowForKey(QETInformation::ELMT_LABEL);
if (formula && label) {
const bool has_formula = !formula->edit->text().isEmpty();
label->edit->setEnabled(!has_formula && label->state != Clear);
label->erase->setEnabled(!has_formula);
}
}
/**
@brief MultiElementInfoWidget::changed
In live edit, every change is applied at once.
*/
void MultiElementInfoWidget::changed()
{
if (m_live_edit && !m_updating) {
apply();
}
}
/**
@brief MultiElementInfoWidget::elementInfoChanged
The information of an element was changed by something else than
this editor (undo, another editor): read the elements again.
*/
void MultiElementInfoWidget::elementInfoChanged()
{
if (m_applying || m_refresh_pending) {
return;
}
//An undo changes every element one after the other:
//read them once, when it is done.
m_refresh_pending = true;
QTimer::singleShot(0, this, [this]() {
m_refresh_pending = false;
updateUi();
});
}
MultiElementInfoWidget::Row *MultiElementInfoWidget::rowForKey(const QString &key) const
{
for (Row *row : m_rows) {
if (row->key == key) {
return row;
}
}
return nullptr;
}
+125
View File
@@ -0,0 +1,125 @@
/*
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 MULTIELEMENTINFOWIDGET_H
#define MULTIELEMENTINFOWIDGET_H
#include "../PropertiesEditor/propertieseditorwidget.h"
#include "../diagramcontext.h"
#include <QHash>
#include <QPointer>
class Element;
class QCheckBox;
class QGraphicsItem;
class QLabel;
class QLineEdit;
class QToolButton;
/**
@brief The MultiElementInfoWidget class
Edit the information (manufacturer, supplier, references...) of
several elements at once (issue #1439).
Each field shows the value when every element has the same one, or
"Multiple values" when they differ. A field left alone keeps each
element's own value, a typed text is given to every element, and the
erase box clears the field on every element. The restore button of a
field, and reset(), bring back the values the elements had.
All the changes of one apply are a single undo step.
*/
class MultiElementInfoWidget : public PropertiesEditorWidget
{
Q_OBJECT
public:
explicit MultiElementInfoWidget(const QList<Element *> &elements,
int other_items = 0,
QWidget *parent = nullptr);
~MultiElementInfoWidget() override;
void setElements(const QList<Element *> &elements, int other_items = 0);
QList<Element *> elements() const;
void apply() override;
void reset() override;
QUndoCommand *associatedUndo() const override;
QString title() const override {return tr("Information");}
void updateUi() override;
bool setLiveEdit(bool live_edit) override;
static bool takesInformations(const Element *element);
static QList<Element *> informationElements(const QList<QGraphicsItem *> &items);
static QList<QGraphicsItem *> withoutOwnTexts(const QList<QGraphicsItem *> &items);
//What a field will do to the elements on apply
enum FieldState {Keep, Set, Clear};
FieldState fieldState(const QString &key) const;
void setFieldText(const QString &key, const QString &text);
void setFieldCleared(const QString &key, bool cleared);
void restoreField(const QString &key);
bool fieldIsMixed(const QString &key) const;
private:
struct Row {
QString key;
QLabel *label = nullptr;
QLineEdit *edit = nullptr;
QCheckBox *erase = nullptr;
QToolButton *restore = nullptr;
QString initial;
bool mixed = false;
FieldState state = Keep;
};
struct Flag {
QString key;
QCheckBox *check = nullptr;
QToolButton *restore = nullptr;
Qt::CheckState initial = Qt::Unchecked;
bool changed = false;
};
void buildInterface();
void takeSnapshot();
void fillRows();
void updateHeader();
void updateSuggestions();
void rowEdited(Row *row);
void rowErased(Row *row);
void restoreRow(Row *row);
void restoreFlag(Flag *flag);
void flagClicked(Flag *flag);
void updateRowLook(Row *row);
void updateLabelRow();
void changed();
void elementInfoChanged();
Row *rowForKey(const QString &key) const;
QList<QPointer<Element>> m_elements;
QHash<Element *, DiagramContext> m_snapshot;
int m_other_items = 0;
QLabel *m_header = nullptr;
QList<Row *> m_rows;
QList<Flag *> m_flags;
bool m_updating = false;
bool m_applying = false;
bool m_refresh_pending = false;
quint64 m_session = 0;
};
#endif // MULTIELEMENTINFOWIDGET_H
+7
View File
@@ -267,6 +267,13 @@ endfunction()
# alone and with a group.
qet_add_app_test(tst_rotateselectionimage tst_rotateselectionimage.cpp)
# Editing the information of several elements at once (#1439): kept, set
# and cleared fields, restore and reset, one undo step, the selection
# panel's editor and the Edit action.
qet_add_app_test(tst_multielementinfo tst_multielementinfo.cpp)
target_compile_definitions(tst_multielementinfo PRIVATE
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
# Accepting the settings dialog writes qet-assistant.json with the
# collection paths just chosen, not the ones they replace.
qet_add_app_test(tst_assistantinfopaths tst_assistantinfopaths.cpp)
+412
View File
@@ -0,0 +1,412 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "qetapptest.h"
#include "diagramcontent.h"
#include "factory/propertieseditorfactory.h"
#include "qetgraphicsitem/conductor.h"
#include "qetgraphicsitem/dynamicelementtextitem.h"
#include "qetgraphicsitem/element.h"
#include "qetinformation.h"
#include "projectview.h"
#include "qetproject.h"
#include "ui/elementinfopartwidget.h"
#include "ui/elementinfowidget.h"
#include "ui/elementpropertieswidget.h"
#include "ui/multielementinfowidget.h"
#include "undocommand/changeelementinformationcommand.h"
#include <QCheckBox>
#include <QTabWidget>
#include <QUndoStack>
// Editing the information of several elements at once (#1439): a field left
// alone keeps each element's own value, a typed text goes to every element,
// the erase box clears the field on every element, the restore button and
// reset() bring back what the elements had, and one apply is one undo step.
class tst_multielementinfo : public QObject
{
Q_OBJECT
QTemporaryDir m_dir;
Diagram *m_diagram = nullptr;
QList<Element *> m_elements;
Conductor *m_conductor = nullptr;
QString info(Element *e, const QString &key) const
{
return e->elementInformations().value(key).toString();
}
QUndoStack &stack() { return m_diagram->undoStack(); }
// The three test elements get known information, as one undo step
// the test then leaves on the stack
void setKnownInfo()
{
QMap<QPointer<Element>, QPair<DiagramContext, DiagramContext>> map;
const QStringList suppliers {"S1", "S2", ""};
for (int i = 0 ; i < 3 ; ++i)
{
Element *e = m_elements.at(i);
DiagramContext info;
info.addValue(QETInformation::ELMT_MANUFACTURER, "Maker");
if (!suppliers.at(i).isEmpty())
info.addValue(QETInformation::ELMT_SUPPLIER, suppliers.at(i));
info.addValue(QETInformation::ELMT_DESCRIPTION, QString("desc %1").arg(i));
info.addValue("my_custom_key", QString("custom %1").arg(i));
if (i == 0)
info.addValue("exclude_from_bom", "true");
map.insert(e, qMakePair(e->elementInformations(), info));
}
stack().push(new ChangeElementInformationCommand(map));
}
private slots:
void initTestCase()
{
QETDiagramEditor *editor = QetAppTest::editor();
QVERIFY(editor);
// The first example folio with three plain elements and a wire
const QStringList files = QDir(QET_EXAMPLES_DIR).entryList({"*.qet"}, QDir::Files, QDir::Name);
for (const QString &file : files)
{
// A copy: opening a project leaves a lock file beside it
const QString copy = m_dir.filePath(file);
QVERIFY(QFile::copy(QDir(QET_EXAMPLES_DIR).filePath(file), copy));
QVERIFY(editor->openAndAddProject(copy, false));
QETProject *project = editor->currentProjectView()->project();
for (Diagram *d : project->diagrams())
{
QList<Element *> simple;
Conductor *conductor = nullptr;
for (QGraphicsItem *qgi : d->items())
{
if (qgi->type() == Element::Type
&& static_cast<Element *>(qgi)->linkType() == Element::Simple)
simple << static_cast<Element *>(qgi);
else if (!conductor && qgi->type() == Conductor::Type)
conductor = static_cast<Conductor *>(qgi);
}
if (simple.size() >= 3 && conductor)
{
m_diagram = d;
m_elements = simple.mid(0, 3);
m_conductor = conductor;
break;
}
}
if (m_diagram)
break;
}
QVERIFY2(m_diagram, "no example folio with three plain elements and a wire");
setKnownInfo();
}
void init()
{
stack().setClean();
}
void cleanup()
{
// Back to the known information
while (stack().index() > stack().cleanIndex())
stack().undo();
}
void showsSharedAndMixedValues()
{
MultiElementInfoWidget w(m_elements);
QCOMPARE(w.fieldIsMixed(QETInformation::ELMT_MANUFACTURER), false);
QCOMPARE(w.fieldIsMixed(QETInformation::ELMT_SUPPLIER), true);
QCOMPARE(w.fieldIsMixed(QETInformation::ELMT_DESCRIPTION), true);
// No element has a value: same (empty) value, not mixed
QCOMPARE(w.fieldIsMixed(QETInformation::ELMT_QUANTITY), false);
QVERIFY(!w.associatedUndo());
}
void setGivesTextToEveryElementAsOneUndo()
{
MultiElementInfoWidget w(m_elements);
w.setFieldText(QETInformation::ELMT_SUPPLIER, "ACME");
QCOMPARE(w.fieldState(QETInformation::ELMT_SUPPLIER), MultiElementInfoWidget::Set);
const int before = stack().index();
w.apply();
QCOMPARE(stack().index(), before + 1);
for (Element *e : m_elements)
{
QCOMPARE(info(e, QETInformation::ELMT_SUPPLIER), QString("ACME"));
// Untouched fields keep each element's own value
QCOMPARE(info(e, QETInformation::ELMT_MANUFACTURER), QString("Maker"));
QCOMPARE(info(e, "my_custom_key"),
QString("custom %1").arg(m_elements.indexOf(e)));
}
QCOMPARE(info(m_elements[0], QETInformation::ELMT_DESCRIPTION), QString("desc 0"));
stack().undo();
QCOMPARE(info(m_elements[0], QETInformation::ELMT_SUPPLIER), QString("S1"));
QCOMPARE(info(m_elements[1], QETInformation::ELMT_SUPPLIER), QString("S2"));
QVERIFY(!m_elements[2]->elementInformations().contains(QETInformation::ELMT_SUPPLIER));
}
void eraseClearsEveryElement()
{
MultiElementInfoWidget w(m_elements);
w.setFieldCleared(QETInformation::ELMT_DESCRIPTION, true);
QCOMPARE(w.fieldState(QETInformation::ELMT_DESCRIPTION), MultiElementInfoWidget::Clear);
w.apply();
for (Element *e : m_elements) {
QVERIFY(!e->elementInformations().contains(QETInformation::ELMT_DESCRIPTION));
QCOMPARE(info(e, QETInformation::ELMT_MANUFACTURER), QString("Maker"));
}
}
// Emptying a field every element shares clears it, as in the
// one-element editor
void emptyingASharedValueClearsIt()
{
MultiElementInfoWidget w(m_elements);
w.setFieldText(QETInformation::ELMT_MANUFACTURER, QString());
QCOMPARE(w.fieldState(QETInformation::ELMT_MANUFACTURER), MultiElementInfoWidget::Set);
w.apply();
for (Element *e : m_elements)
QVERIFY(!e->elementInformations().contains(QETInformation::ELMT_MANUFACTURER));
}
// A mixed field emptied again, or a shared one typed back to its
// value, changes nothing
void typingBackToTheValueKeeps()
{
MultiElementInfoWidget w(m_elements);
w.setFieldText(QETInformation::ELMT_SUPPLIER, "x");
w.setFieldText(QETInformation::ELMT_SUPPLIER, QString());
QCOMPARE(w.fieldState(QETInformation::ELMT_SUPPLIER), MultiElementInfoWidget::Keep);
w.setFieldText(QETInformation::ELMT_MANUFACTURER, "Make");
w.setFieldText(QETInformation::ELMT_MANUFACTURER, "Maker");
QCOMPARE(w.fieldState(QETInformation::ELMT_MANUFACTURER), MultiElementInfoWidget::Keep);
QVERIFY(!w.associatedUndo());
}
void restoreAndResetBringBackTheValues()
{
MultiElementInfoWidget w(m_elements);
w.setFieldText(QETInformation::ELMT_SUPPLIER, "ACME");
w.setFieldCleared(QETInformation::ELMT_DESCRIPTION, true);
w.restoreField(QETInformation::ELMT_SUPPLIER);
QCOMPARE(w.fieldState(QETInformation::ELMT_SUPPLIER), MultiElementInfoWidget::Keep);
QCOMPARE(w.fieldState(QETInformation::ELMT_DESCRIPTION), MultiElementInfoWidget::Clear);
w.reset();
QCOMPARE(w.fieldState(QETInformation::ELMT_DESCRIPTION), MultiElementInfoWidget::Keep);
QVERIFY(!w.associatedUndo());
}
// The selection panel applies each keystroke; they make one undo
// step, and restoring the field in the panel undoes it
void liveEditMergesAndRestores()
{
MultiElementInfoWidget w(m_elements);
w.setLiveEdit(true);
const int before = stack().index();
w.setFieldText(QETInformation::ELMT_SUPPLIER, "A");
w.setFieldText(QETInformation::ELMT_SUPPLIER, "AC");
w.setFieldText(QETInformation::ELMT_SUPPLIER, "ACME");
QCOMPARE(stack().index(), before + 1);
for (Element *e : m_elements)
QCOMPARE(info(e, QETInformation::ELMT_SUPPLIER), QString("ACME"));
w.restoreField(QETInformation::ELMT_SUPPLIER);
QCOMPARE(info(m_elements[0], QETInformation::ELMT_SUPPLIER), QString("S1"));
QCOMPARE(info(m_elements[1], QETInformation::ELMT_SUPPLIER), QString("S2"));
QVERIFY(!m_elements[2]->elementInformations().contains(QETInformation::ELMT_SUPPLIER));
}
// The first live change never joins an earlier change to the
// same elements (here the paste-like command of initTestCase)
void liveEditDoesNotJoinEarlierChange()
{
QMap<QPointer<Element>, QPair<DiagramContext, DiagramContext>> map;
for (Element *e : m_elements) {
DiagramContext info = e->elementInformations();
info.addValue(QETInformation::ELMT_PLANT, "P1");
map.insert(e, qMakePair(e->elementInformations(), info));
}
stack().push(new ChangeElementInformationCommand(map));
const int before = stack().index();
MultiElementInfoWidget w(m_elements);
w.setLiveEdit(true);
w.setFieldText(QETInformation::ELMT_SUPPLIER, "ACME");
QCOMPARE(stack().index(), before + 1);
stack().undo();
for (Element *e : m_elements)
QCOMPARE(info(e, QETInformation::ELMT_PLANT), QString("P1"));
}
// An undo from outside the editor shows again what the elements have
void undoRefreshesTheEditor()
{
MultiElementInfoWidget w(m_elements);
w.setLiveEdit(true);
w.setFieldText(QETInformation::ELMT_SUPPLIER, "ACME");
QCOMPARE(w.fieldState(QETInformation::ELMT_SUPPLIER), MultiElementInfoWidget::Set);
stack().undo();
QCoreApplication::processEvents();
QCOMPARE(w.fieldIsMixed(QETInformation::ELMT_SUPPLIER), true);
QCOMPARE(w.fieldState(QETInformation::ELMT_SUPPLIER), MultiElementInfoWidget::Keep);
}
// "Exclude from the bill of materials": partly checked when the
// elements differ, a click sets it on every element
void flagMixedThenSet()
{
MultiElementInfoWidget w(m_elements);
QCheckBox *bom = nullptr;
for (QCheckBox *cb : w.findChildren<QCheckBox *>())
if (cb->text() == QObject::tr("Exclude from the bill of materials"))
bom = cb;
QVERIFY(bom);
QCOMPARE(bom->checkState(), Qt::PartiallyChecked);
bom->click();
w.apply();
for (Element *e : m_elements)
QVERIFY(QET::infoFlagIsTrue(info(e, "exclude_from_bom")));
}
void formulaIdKeptOnlyWithItsFormula()
{
QMap<QPointer<Element>, QPair<DiagramContext, DiagramContext>> map;
for (Element *e : m_elements)
{
DiagramContext info = e->elementInformations();
info.addValue(QETInformation::ELMT_FORMULA, "K%{id}");
info.addValue(QETInformation::ELMT_FORMULA_ID, "{00000000-0000-0000-0000-000000000001}", false);
map.insert(e, qMakePair(e->elementInformations(), info));
}
// Element drops an id naming no numbering of the project;
// what is checked is that the editor itself keeps or drops it
stack().push(new ChangeElementInformationCommand(map));
const bool stored = m_elements[0]->elementInformations().contains(QETInformation::ELMT_FORMULA_ID);
MultiElementInfoWidget w(m_elements);
w.setFieldText(QETInformation::ELMT_SUPPLIER, "ACME");
w.apply();
QCOMPARE(m_elements[0]->elementInformations().contains(QETInformation::ELMT_FORMULA_ID), stored);
MultiElementInfoWidget w2(m_elements);
w2.setFieldText(QETInformation::ELMT_FORMULA, "Q%{id}");
w2.apply();
for (Element *e : m_elements) {
QVERIFY(!e->elementInformations().contains(QETInformation::ELMT_FORMULA_ID));
QCOMPARE(info(e, QETInformation::ELMT_FORMULA), QString("Q%{id}"));
}
}
// The selection panel: one element, several, and a mix
void factoryPicksTheEditor()
{
QList<QGraphicsItem *> one {m_elements[0]};
std::unique_ptr<PropertiesEditorWidget> e1(PropertiesEditorFactory::propertiesEditor(one, nullptr));
QVERIFY(qobject_cast<ElementPropertiesWidget *>(e1.get()));
QList<QGraphicsItem *> two {m_elements[0], m_elements[1]};
std::unique_ptr<PropertiesEditorWidget> e2(PropertiesEditorFactory::propertiesEditor(two, nullptr));
auto multi = qobject_cast<MultiElementInfoWidget *>(e2.get());
QVERIFY(multi);
QCOMPARE(multi->elements().size(), 2);
// Same kind of editor: reused for the new selection
QList<QGraphicsItem *> mix {m_elements[0], m_elements[1], m_elements[2], m_conductor};
PropertiesEditorWidget *e3 = PropertiesEditorFactory::propertiesEditor(mix, e2.get());
QCOMPARE(e3, e2.get());
QCOMPARE(multi->elements().size(), 3);
// Select all takes the elements' own texts: not other items
QList<QGraphicsItem *> with_texts {m_elements[0], m_elements[1]};
for (QGraphicsItem *child : m_elements[0]->childItems())
if (child->type() == DynamicElementTextItem::Type)
with_texts << child;
QVERIFY(with_texts.size() > 2);
QCOMPARE(MultiElementInfoWidget::withoutOwnTexts(with_texts).size(), 2);
// A text alone, or with another element, stays
QList<QGraphicsItem *> other_text {m_elements[1], with_texts.last()};
QCOMPARE(MultiElementInfoWidget::withoutOwnTexts(other_text).size(), 2);
QList<QGraphicsItem *> wire {m_conductor};
std::unique_ptr<PropertiesEditorWidget> e4(PropertiesEditorFactory::propertiesEditor(wire, nullptr));
QVERIFY(!qobject_cast<MultiElementInfoWidget *>(e4.get()));
}
// Edit (Ctrl+E) is offered for several elements, not for a mix
void editActionForSeveralElements()
{
QETDiagramEditor *editor = QetAppTest::editor();
QAction *edit = QetAppTest::action("edit_selected_element");
QVERIFY(edit);
m_diagram->clearSelection();
m_elements[0]->setSelected(true);
m_elements[1]->setSelected(true);
editor->slot_updateComplexActions();
QVERIFY(edit->isEnabled());
// with their own texts, as select all gives them
for (QGraphicsItem *child : m_elements[0]->childItems())
child->setSelected(true);
editor->slot_updateComplexActions();
QVERIFY(edit->isEnabled());
m_conductor->setSelected(true);
editor->slot_updateComplexActions();
QVERIFY(!edit->isEnabled());
m_diagram->clearSelection();
}
// The selection panel builds a new editor when the selection goes
// from one element to several and back: it opens on the tab
// picked last there, not on the first one
void panelKeepsTheTabPicked()
{
auto tabsOf = [](QWidget *w) { return w->findChild<QTabWidget *>(); };
int general = -1;
{
ElementPropertiesWidget first(m_elements[0]);
first.setLiveEdit(true);
QTabWidget *tabs = tabsOf(&first);
QVERIFY(tabs);
for (int i = 0 ; i < tabs->count() ; ++i)
if (tabs->tabText(i) == QObject::tr("General"))
general = i;
QVERIFY(general > 0);
QVERIFY(tabs->currentIndex() != general);
emit tabs->tabBarClicked(general);
tabs->setCurrentIndex(general);
}
ElementPropertiesWidget second(m_elements[1]);
second.setLiveEdit(true);
QCOMPARE(tabsOf(&second)->currentIndex(), general);
// A dialog (not live) still opens on the first tab
ElementPropertiesWidget dialog(m_elements[1]);
QCOMPARE(tabsOf(&dialog)->currentIndex(), 0);
}
// The Reset button of the one-element dialog brings back the
// Information tab's values
void oneElementResetRestores()
{
ElementInfoWidget w(m_elements[0]);
ElementInfoPartWidget *part = nullptr;
for (auto *p : w.findChildren<ElementInfoPartWidget *>())
if (p->key() == QETInformation::ELMT_SUPPLIER)
part = p;
QVERIFY(part);
part->setText("changed");
QCOMPARE(w.currentInfo().value(QETInformation::ELMT_SUPPLIER).toString(), QString("changed"));
w.reset();
QCOMPARE(w.currentInfo().value(QETInformation::ELMT_SUPPLIER).toString(), QString("S1"));
}
};
QET_APP_TEST_MAIN(tst_multielementinfo)
#include "tst_multielementinfo.moc"