Add cable management: drawing, cores, numbering, cross-references and cable list

Cables are drawn line objects (toolbar button next to the auto
break/reconnect button, click-move-click, right click cancels), not
components. The type comes from a CSV catalog following the material
list pattern (designation, cores, core colours, editable through the
entry dialog with "Nouvelle entrée" / "Modifier l'entrée").

Each core is bound to the wire its colour label stands on, and the
cable reference is generated per core and written into the Conductor
entry via Conductor::setCableReference(label, cable uuid, slot). The
conductor entry is where it has to live because the terminal strip
plan (Klemmenplan) consumes it in the next step.

Numbering: "Cables" rule under Programmeinstellungen/Neues
Projekt/Nummerierung auto (mirrored in project properties), literal
"W" fallback without a rule, French gate dialog when no rule exists,
Nummerierung menu with the renumber dialog (preferred axis, per-folio
counter, rule and counter state written back, hand-typed names ask,
whole-project scope).

Also included: label block and under-line texts with per-text font and
alignment plus the material-list cell margins, free-core placement and
right-click removal, claim questions for wires of other cables,
multi-selection drag as one undo step, type change with confirmation,
cross-references for type "Cable" (%f-%l%c, own font, clickable links
in exported PDFs), report propagation of the cable definition,
Listes menu (table of contents, material list, terminal strip manager,
terminal generator plugin, cable list), cable list with freely
selectable columns (installation/localisation of cable, start and end,
Blatt start/end, length, used cores...), CSV export dialog with column
pick and preview in the Projekt menu, script API (addTable kind
"cable_list", exportCableList) and a unit test for the type catalog.
This commit is contained in:
Kellermorph
2026-10-08 15:56:00 +02:00
parent 1ca13ce62d
commit bf9b34398d
108 changed files with 18482 additions and 293 deletions
+155
View File
@@ -0,0 +1,155 @@
/*
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 3 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 "cableexportdialog.h"
#include "cablelistmodel.h"
#include "cablelistrows.h"
#include "cablequerywidget.h"
#include "../bomexport.h"
#include "../qet.h"
#include "../qetapp.h"
#include "../qetproject.h"
#include <QAbstractItemView>
#include <QCheckBox>
#include <QDialogButtonBox>
#include <QFileDialog>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QMessageBox>
#include <QPushButton>
#include <QTableView>
#include <QVBoxLayout>
/**
@brief CableExportDialog::CableExportDialog
@param project the project whose cables are exported
@param parent
*/
CableExportDialog::CableExportDialog(QETProject *project, QWidget *parent) :
QDialog(parent),
m_project(project)
{
setWindowTitle(tr("Exporter la liste des câbles au format CSV"));
QET::trackDialogGeometry(this);
auto *main_layout = new QVBoxLayout(this);
//The same column picker the cable list is built with, so the
//export asks for its columns the same way the list does
m_query_widget = new CableQueryWidget(this);
main_layout->addWidget(m_query_widget);
auto *page_group = new QGroupBox(tr("Mise en page"), this);
auto *page_layout = new QVBoxLayout(page_group);
m_include_headers = new QCheckBox(tr("inclure les en-têtes"), page_group);
m_include_headers->setChecked(true);
page_layout->addWidget(m_include_headers);
page_layout->addStretch();
main_layout->addWidget(page_group);
auto *preview_layout = new QHBoxLayout;
auto *preview_pb = new QPushButton(tr("Aperçu"), this);
preview_layout->addWidget(preview_pb);
preview_layout->addStretch();
main_layout->addLayout(preview_layout);
m_preview_table = new QTableView(this);
m_preview_table->setEditTriggers(QAbstractItemView::NoEditTriggers);
main_layout->addWidget(m_preview_table);
auto *buttons = new QDialogButtonBox(
QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
main_layout->addWidget(buttons);
connect(buttons, &QDialogButtonBox::accepted,
this, &CableExportDialog::accept);
connect(buttons, &QDialogButtonBox::rejected,
this, &CableExportDialog::reject);
connect(preview_pb, &QPushButton::clicked,
this, &CableExportDialog::preview);
m_model = new CableListModel(m_project, this);
m_preview_table->setModel(m_model);
preview();
setMinimumSize(QSize(760, 560));
}
/**
@brief CableExportDialog::exec
Ask for the file once the user accepted, and write the chosen
columns to it -- the same flow as the material list export
@return
*/
int CableExportDialog::exec()
{
const auto r = QDialog::exec();
if (r == QDialog::Accepted)
{
QString dir = m_project->currentDir();
if (dir.isEmpty()) dir = QETApp::documentDir();
const QString file_name = dir + QLatin1Char('/')
+ tr("liste_cables_") + m_project->title()
+ QStringLiteral(".csv");
const QString file_path = QFileDialog::getSaveFileName(
this, tr("Enregistrer sous... "), file_name,
tr("Fichiers csv (*.csv)"));
if (!file_path.isEmpty())
{
QString error;
const auto csv = CableList::toCsv(
m_project, selectedColumns(),
m_include_headers->isChecked());
if (!BomExport::writeCsv(file_path, csv, &error)) {
QMessageBox::critical(
this, tr("Erreur"),
tr("Impossible d'enregistrer la liste des câbles dans %1.\n%2")
.arg(file_path, error));
}
}
}
return r;
}
/**
@brief CableExportDialog::selectedColumns
@return the picked columns, and the default ones when the user
picked nothing -- an export of everything is what a dialog
confirmed without choosing asks for, the same fallback the drawn
table uses
*/
QStringList CableExportDialog::selectedColumns() const
{
auto keys = m_query_widget->selectedKeys();
if (keys.isEmpty()) {
keys = CableList::defaultKeys();
}
return keys;
}
/**
@brief CableExportDialog::preview
Show what will be written: the chosen columns, worked out from the
cables of the project as they are right now
*/
void CableExportDialog::preview()
{
m_model->setFields(selectedColumns());
m_preview_table->resizeColumnsToContents();
}
+58
View File
@@ -0,0 +1,58 @@
/*
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 3 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 CABLEEXPORTDIALOG_H
#define CABLEEXPORTDIALOG_H
#include <QCoreApplication>
#include <QDialog>
class QCheckBox;
class QTableView;
class CableListModel;
class CableQueryWidget;
class QETProject;
/**
@brief The CableExportDialog asks which columns of the cable list
are exported and writes them to a CSV file, built the same way the
BOMExportDialog does it for the material list: the column picker
the cable list is built with on top, the page settings under it, a
preview of what will be written, and the file asked for when the
dialog is accepted.
*/
class CableExportDialog : public QDialog
{
Q_DECLARE_TR_FUNCTIONS(CableExportDialog)
public:
explicit CableExportDialog(QETProject *project, QWidget *parent = nullptr);
int exec() override;
private:
QStringList selectedColumns() const;
void preview();
QETProject *m_project;
CableQueryWidget *m_query_widget;
CableListModel *m_model;
QCheckBox *m_include_headers;
QTableView *m_preview_table;
};
#endif // CABLEEXPORTDIALOG_H
+421
View File
@@ -0,0 +1,421 @@
/*
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 3 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 "cablelistmodel.h"
#include "cablelistrows.h"
#include "../cable/cable.h"
#include "../diagram.h"
#include "../qetgraphicsitem/element.h"
#include "../qetproject.h"
#include "../qetxml.h"
#include "../utils/qetutils.h"
#include <QFont>
#include <QMetaEnum>
#include <algorithm>
#include <utility>
/**
@brief CableListModel::CableListModel
@param project : project whose cables this model lists
@param parent : parent QObject
*/
CableListModel::CableListModel(QETProject *project, QObject *parent) :
QAbstractTableModel(parent),
m_project(project)
{
//Left is what a table starts with and stays with: the factory
//overwrites it with the alignment chosen in the dialog, and a
//table which never got that far (no column selected at
//placement) still draws its cells left rather than from an
//empty role
m_index_0_0_data.insert(Qt::TextAlignmentRole, int(Qt::AlignLeft));
//The same gap between the text and the cell lines the creation
//dialog offers for the material list, already in place for a
//table which starts out empty and gets its columns later --
//the factory overwrites it with the margins chosen in the
//dialog as soon as the table has a cell to configure
m_index_0_0_data.insert(Qt::UserRole+1,
QETUtils::marginsToString(QMargins(5,5,10,5)));
connectProject();
setFields(CableList::defaultKeys());
}
/**
@brief CableListModel::CableListModel
@param other_model
*/
CableListModel::CableListModel(const CableListModel &other_model) :
QAbstractTableModel(other_model.parent())
{
setParent(other_model.parent());
m_project = other_model.m_project;
m_identifier = other_model.m_identifier;
connectProject();
setFields(other_model.m_fields);
m_header_data = other_model.m_header_data;
m_index_0_0_data = other_model.m_index_0_0_data;
}
/**
@brief CableListModel::rowCount
Reimplemented for QAbstractTableModel
@param parent
@return
*/
int CableListModel::rowCount(const QModelIndex &parent) const
{
if (parent.isValid())
return 0;
//A table built without a single column shows no rows either:
//the empty frame a nomenclature table makes when it is
//created without entries, not a list of rows nobody can see
if (m_fields.isEmpty()) {
return 0;
}
return m_rows.count();
}
/**
@brief CableListModel::columnCount
Reimplemented for QAbstractTableModel
@param parent
@return
*/
int CableListModel::columnCount(const QModelIndex &parent) const
{
if (parent.isValid())
return 0;
return m_fields.count();
}
/**
@brief CableListModel::setHeaderData
Reimplemented from QAbstractTableModel.
Only horizontal orientation is accepted.
@param section
@param orientation
@param value
@param role
@return
*/
bool CableListModel::setHeaderData(int section, Qt::Orientation orientation,
const QVariant &value, int role)
{
if (orientation == Qt::Vertical) {
return false;
}
auto hash_ = m_header_data.value(section);
hash_.insert(role, value);
m_header_data.insert(section, hash_);
emit headerDataChanged(orientation, section, section);
return true;
}
/**
@brief CableListModel::headerData
Reimplemented for QAbstractTableModel
@param section
@param orientation
@param role
@return
*/
QVariant CableListModel::headerData(int section, Qt::Orientation orientation, int role) const
{
if (orientation == Qt::Vertical) {
return QVariant();
}
if (m_header_data.contains(section))
{
auto hash_ = m_header_data.value(section);
if (role == Qt::DisplayRole && !hash_.contains(Qt::DisplayRole)) { //special case to have the same behavior as Qt
return hash_.value(Qt::EditRole);
}
return m_header_data.value(section).value(role);
}
return QVariant();
}
/**
@brief CableListModel::setData
Only store the data for the index 0.0. An empty table has no cells
yet -- index(0,0) does not exist without a single column -- but
its look is still settable: the factory configuring a freshly made
table must not be thrown away just because the table was placed
without entries and gains its columns afterwards. The data is read
as soon as cells exist.
@param index
@param value
@param role
@return
*/
bool CableListModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
if (index.isValid() && (index.row() != 0 || index.column() != 0)) {
return false;
}
m_index_0_0_data.insert(role, value);
if (index.isValid()) {
emit dataChanged(index, index, {role});
}
return true;
}
/**
@brief CableListModel::data
Reimplemented for QAbstractTableModel
@param index
@param role
@return
*/
QVariant CableListModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid())
return QVariant();
if (index.row() == 0 &&
index.column() == 0 &&
role != Qt::DisplayRole) {
return m_index_0_0_data.value(role);
}
if (role == Qt::DisplayRole) {
return m_rows.at(index.row()).value(m_fields.at(index.column()));
}
return QVariant();
}
/**
@brief CableListModel::project
@return the project whose cables this model lists
*/
QETProject *CableListModel::project() const
{
return m_project.data();
}
/**
@brief CableListModel::setFields
Set which columns this table shows, in that order, and work the rows
out along the way. Unknown keys are dropped rather than shown as
empty columns: a file naming a column this program does not know
would draw a blank header nobody can fill.
@param fields the keys of CableList::allColumns()
*/
void CableListModel::setFields(const QStringList &fields)
{
QStringList kept;
for (const QString &field : fields)
{
if (kept.contains(field)) {
continue;
}
if (CableList::labelOf(field).isEmpty()) {
continue;
}
kept << field;
}
beginResetModel();
m_fields = kept;
//The columns themselves change, their labels are written again
//below -- but HOW a header is drawn (font, alignment, margins)
//belongs to the table, not to the column set, and stays as it
//is across a column edit. Sections which no longer exist are
//dropped, so a stale header of an old column cannot linger
for (auto it = m_header_data.begin(); it != m_header_data.end(); ) {
if (it.key() >= m_fields.count()) {
it = m_header_data.erase(it);
} else {
++it;
}
}
for (int i = 0; i < m_fields.count(); ++i) {
m_header_data[i].insert(Qt::DisplayRole, CableList::labelOf(m_fields.at(i)));
}
m_rows = m_project ? CableList::rows(m_project)
: QVector<QMap<QString, QString>>();
endResetModel();
}
/**
@brief CableListModel::queryString
@return the fields joined the way the project file stores them
*/
QString CableListModel::queryString() const
{
return m_fields.join(QLatin1Char(','));
}
/**
@brief CableListModel::refresh
Work the rows out again, and watch everything they are worked out
from: a cable of the project, a folio, a piece a cable lands on
(its label is the BMK of an end, its Installation and Localisation
are what those two columns of an end read).
*/
void CableListModel::refresh()
{
for (const QMetaObject::Connection &connection : std::as_const(m_watchers)) {
QObject::disconnect(connection);
}
m_watchers.clear();
if (m_project)
{
const QVector<Cable *> cables = m_project->cables();
for (Cable *cable : cables) {
m_watchers << connect(cable, &Cable::changed,
this, &CableListModel::refresh);
}
const QList<Diagram *> diagrams = m_project->diagrams();
for (Diagram *diagram : diagrams)
{
m_watchers << connect(diagram, &Diagram::diagramInformationChanged,
this, &CableListModel::refresh);
const QList<Element *> elements = diagram->elements();
for (Element *element : elements) {
m_watchers << connect(element, &Element::elementInfoChange,
this, &CableListModel::refresh);
}
}
}
beginResetModel();
m_rows = m_project ? CableList::rows(m_project)
: QVector<QMap<QString, QString>>();
endResetModel();
}
/**
@brief CableListModel::connectProject
Watch the project itself: cables and folios coming and going reach
this model even though no refresh of ours was running to catch them
(the rows of a project being read from its file are filled cable by
cable, one signal at a time).
*/
void CableListModel::connectProject()
{
if (!m_project) {
return;
}
connect(m_project, &QETProject::cableAdded, this, &CableListModel::refresh);
connect(m_project, &QETProject::cableRemoved, this, &CableListModel::refresh);
connect(m_project, &QETProject::diagramAdded, this, &CableListModel::refresh);
connect(m_project, &QETProject::diagramRemoved, this, &CableListModel::refresh);
}
/**
@brief CableListModel::toXml
Save the model to xml: which columns are shown, the look of the
cells and of the header -- the rows themselves are not saved, they
are worked out again from the cables whenever the project opens.
@param document
@return
*/
QDomElement CableListModel::toXml(QDomDocument &document) const
{
auto dom_element = document.createElement(xmlTagName());
//Identifier
auto dom_identifier = document.createElement("identifier");
auto dom_identifier_text = document.createTextNode(m_identifier);
dom_identifier.appendChild(dom_identifier_text);
dom_element.appendChild(dom_identifier);
//Fields
auto dom_fields = document.createElement("fields");
auto dom_fields_text = document.createTextNode(queryString());
dom_fields.appendChild(dom_fields_text);
dom_element.appendChild(dom_fields);
//Add index 0,0 data
auto index_00 = document.createElement("index00");
index_00.setAttribute("font", QETUtils::fontToString(m_index_0_0_data.value(Qt::FontRole).value<QFont>()));
auto me = QMetaEnum::fromType<Qt::Alignment>();
index_00.setAttribute("alignment", me.valueToKey(m_index_0_0_data.value(Qt::TextAlignmentRole).toInt()));
dom_element.appendChild(index_00);
index_00.setAttribute("margins", m_index_0_0_data.value(Qt::UserRole+1).toString());
//header data
QHash<int, QList<int>> horizontal_;
for (auto key : m_header_data.keys())
{
//We save all data except the display role, because it is
//generated from the fields above (same reason and same sorted
//order as ProjectDBModel, so the file stays reproducible)
auto list = m_header_data.value(key).keys();
list.removeAll(Qt::DisplayRole);
std::sort(list.begin(), list.end());
horizontal_.insert(key, list);
}
dom_element.appendChild(QETXML::modelHeaderDataToXml(document, this, horizontal_, QHash<int, QList<int>>()));
return dom_element;
}
/**
@brief CableListModel::fromXml
Restore the model from xml
@param element
*/
void CableListModel::fromXml(const QDomElement &element)
{
if (element.tagName() != xmlTagName())
return;
setIdentifier(element.firstChildElement("identifier").text());
setFields(element.firstChildElement("fields").text()
.split(QLatin1Char(','), Qt::SkipEmptyParts));
//Index 0,0
auto index_00 = element.firstChildElement("index00");
QFont font_;
QETUtils::fontFromString(font_, index_00.attribute("font"));
m_index_0_0_data.insert(Qt::FontRole, font_);
auto me = QMetaEnum::fromType<Qt::Alignment>();
const int alignment = me.keyToValue(index_00.attribute("alignment")
.toStdString().data());
//A file written without an alignment (an empty or unknown enum
//key) would restore as -1, whose bits draw every cell right-
//aligned -- left is the default instead
m_index_0_0_data.insert(Qt::TextAlignmentRole,
alignment > 0 ? alignment : int(Qt::AlignLeft));
//A file saved without cell margins (one written before the
//look of an empty table was kept) restores with the same
//default the creation dialog offers, instead of text drawn
//against the cell lines
const QString margins_ = index_00.attribute("margins");
m_index_0_0_data.insert(Qt::UserRole+1,
margins_.isEmpty()
? QETUtils::marginsToString(QMargins(5,5,10,5))
: margins_);
QETXML::modelHeaderDataFromXml(element.firstChildElement("header_data"), this);
}
+107
View File
@@ -0,0 +1,107 @@
/*
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 3 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 CABLELISTMODEL_H
#define CABLELISTMODEL_H
#include <QAbstractTableModel>
#include <QDomElement>
#include <QHash>
#include <QList>
#include <QMap>
#include <QPointer>
#include <QStringList>
#include <QVector>
class Cable;
class Diagram;
class Element;
class QETProject;
/**
@brief The CableListModel class
The rows of a cable list, shown by QetGraphicsTableItem the way the
project database model shows a nomenclature -- and shaped after it
on purpose: same roles, same header behaviour, same way of saving
itself to the project file, so that the table on the sheet treats
this model exactly like any other.
Whereas ProjectDBModel asks the project database, this model asks
CableList::rows(), straight from the cables of the project. It
refreshes itself whenever anything those rows are worked out from
changes: a cable edited, a cable added or removed, a folio added,
removed or re-informed (its title block carries the installation and
the place), a label typed over on a piece a cable lands on.
*/
class CableListModel : public QAbstractTableModel
{
Q_OBJECT
public:
explicit CableListModel(QETProject *project, QObject *parent = nullptr);
CableListModel(const CableListModel &other_model);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
bool setHeaderData(int section,
Qt::Orientation orientation,
const QVariant &value,
int role = Qt::EditRole) override;
QVariant headerData(int section,
Qt::Orientation orientation,
int role = Qt::DisplayRole) const override;
bool setData(const QModelIndex &index,
const QVariant &value,
int role = Qt::EditRole) override;
QVariant data(const QModelIndex &index,
int role = Qt::DisplayRole) const override;
/// The columns this table shows, in order: keys of CableList
void setFields(const QStringList &fields);
QStringList fields() const {return m_fields;}
/// The same, joined the way the project file stores it
QString queryString() const;
void setIdentifier(const QString &identifier) {m_identifier = identifier;}
QString identifier() const {return m_identifier;}
QETProject *project() const;
QDomElement toXml(QDomDocument &document) const;
void fromXml(const QDomElement &element);
static QString xmlTagName() {return QStringLiteral("cable_list_model");}
public slots:
/// Work the rows out again and tell whoever shows them
void refresh();
private:
/// Watch everything the rows are worked out from
void connectProject();
QPointer<QETProject> m_project;
QStringList m_fields;
QVector<QMap<QString, QString>> m_rows;
//First int = section, second int = Qt::role, QVariant = value
QHash<int, QHash<int, QVariant>> m_header_data;
QHash<int, QVariant> m_index_0_0_data;
QString m_identifier = QStringLiteral("cable_list");
/// The current watchers of this model, dropped and taken
/// again on every refresh
QList<QMetaObject::Connection> m_watchers;
};
#endif // CABLELISTMODEL_H
@@ -0,0 +1,100 @@
/*
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 3 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 "cablelistmodelpropertieswidget.h"
#include "cablelistmodel.h"
#include "cablequerywidget.h"
#include <QDialog>
#include <QDialogButtonBox>
#include <QHBoxLayout>
#include <QPushButton>
#include <QVBoxLayout>
/**
@brief CableListModelPropertiesWidget::CableListModelPropertiesWidget
@param model
@param parent
*/
CableListModelPropertiesWidget::CableListModelPropertiesWidget(
CableListModel *model,
QWidget *parent) :
PropertiesEditorWidget(parent)
{
auto *layout = new QHBoxLayout(this);
m_edit_pb = new QPushButton(tr("Modifier les colonnes..."), this);
m_refresh_pb = new QPushButton(tr("Rafraîchir"), this);
layout->addWidget(m_edit_pb);
layout->addWidget(m_refresh_pb);
layout->addStretch();
connect(m_edit_pb, &QPushButton::clicked, this, &CableListModelPropertiesWidget::editColumns);
connect(m_refresh_pb, &QPushButton::clicked, this, &CableListModelPropertiesWidget::refreshRows);
setModel(model);
}
/**
@brief CableListModelPropertiesWidget::setModel
@param model
*/
void CableListModelPropertiesWidget::setModel(CableListModel *model)
{
m_model = model;
m_edit_pb->setEnabled(m_model != nullptr);
m_refresh_pb->setEnabled(m_model != nullptr);
}
/**
@brief CableListModelPropertiesWidget::editColumns
Ask for the columns again and put them on the table right away --
the same immediate, not-undoable way the database table changes its
query
*/
void CableListModelPropertiesWidget::editColumns()
{
if (!m_model) {
return;
}
QDialog d(this);
d.setWindowTitle(tr("Colonnes de la liste des câbles"));
auto *layout = new QVBoxLayout(&d);
auto *widget = new CableQueryWidget(&d);
widget->setFields(m_model->fields());
layout->addWidget(widget);
auto *button_box = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
layout->addWidget(button_box);
connect(button_box, &QDialogButtonBox::accepted, &d, &QDialog::accept);
connect(button_box, &QDialogButtonBox::rejected, &d, &QDialog::reject);
if (d.exec()) {
m_model->setFields(widget->selectedKeys());
}
}
/**
@brief CableListModelPropertiesWidget::refreshRows
*/
void CableListModelPropertiesWidget::refreshRows()
{
if (m_model) {
m_model->refresh();
}
}
@@ -0,0 +1,55 @@
/*
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 3 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 CABLELISTMODELPROPERTIESWIDGET_H
#define CABLELISTMODELPROPERTIESWIDGET_H
#include "../../PropertiesEditor/propertieseditorwidget.h"
class CableListModel;
class QPushButton;
/**
@brief The CableListModelPropertiesWidget class
The editor shown when a cable list table is selected: change which
columns the table shows, or work its rows out again. Shaped after
ProjectDBModelPropertiesWidget, which changes its query the same
immediate, not-undoable way -- the table itself is undoable to move
or delete, its content always follows the cables.
*/
class CableListModelPropertiesWidget : public PropertiesEditorWidget
{
Q_OBJECT
public:
explicit CableListModelPropertiesWidget(CableListModel *model = nullptr,
QWidget *parent = nullptr);
~CableListModelPropertiesWidget() override = default;
void setModel(CableListModel *model);
private slots:
void editColumns();
void refreshRows();
private:
CableListModel *m_model = nullptr;
QPushButton *m_edit_pb = nullptr;
QPushButton *m_refresh_pb = nullptr;
};
#endif // CABLELISTMODELPROPERTIESWIDGET_H
+468
View File
@@ -0,0 +1,468 @@
/*
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 3 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 "cablelistrows.h"
#include "../bomexport.h"
#include "../cable/cable.h"
#include "../conductorproperties.h"
#include "../diagram.h"
#include "../qetgraphicsitem/conductor.h"
#include "../qetgraphicsitem/element.h"
#include "../qetgraphicsitem/terminal.h"
#include "../qetinformation.h"
#include "../qetproject.h"
#include <QHash>
#include <QLineF>
#include <algorithm>
#include <climits>
#include <utility>
namespace {
/**
@brief One place where the line of a cable ends.
An end which lands on a piece carries that piece (its uuid for
grouping and its pointer for reading the label, both valid as long
as the plan lives); an end taken from the drawn line carries none,
and its tie names the section instead.
*/
struct CableEnd
{
Diagram *diagram = nullptr;
QPointF scene;
Element *element = nullptr;
QUuid element_uuid;
QString tie;
bool isLanding() const {return !element_uuid.isNull();}
};
/**
@brief endBefore
The order the ends of the cables are read in: the earlier sheet
first, then the higher point of that sheet, then the left one, then
a stable name so that a redraw can never swap two ends and shuffle
the rows around.
@param a first end
@param b second end
@param project the project the sheet numbers come from
@return true when a comes before b
*/
bool endBefore(const CableEnd &a, const CableEnd &b, const QETProject *project)
{
const int folio_a = a.diagram ? project->folioIndex(a.diagram) : INT_MAX;
const int folio_b = b.diagram ? project->folioIndex(b.diagram) : INT_MAX;
if (folio_a != folio_b) {
return folio_a < folio_b;
}
if (a.scene.y() != b.scene.y()) {
return a.scene.y() < b.scene.y();
}
if (a.scene.x() != b.scene.x()) {
return a.scene.x() < b.scene.x();
}
return a.tie < b.tie;
}
/**
@brief folioOf
@param end
@return the sheet number the end lies on, empty when it lies on no
sheet at all (a cable nothing was ever drawn for)
*/
QString folioOf(const CableEnd &end, const QETProject *project)
{
return end.diagram ? QString::number(project->folioIndex(end.diagram) + 1)
: QString();
}
/**
@brief plantOf
@return the Installation the piece the end lands on carries -- the
same field every other list reads for a piece. A piece without one
leaves the cell empty: there is no second place to look for it, and
an end of the drawn line, which lands on no piece at all, fills
nothing either.
*/
QString plantOf(const CableEnd &end)
{
if (!end.element) {
return QString();
}
return end.element->elementInformations()
.value(QETInformation::ELMT_PLANT).toString();
}
/**
@brief locmachOf
@return the Localisation the piece the end lands on carries, with
the same rule as the Installation: what the piece carries or
nothing at all.
*/
QString locmachOf(const CableEnd &end)
{
if (!end.element) {
return QString();
}
return end.element->elementInformations()
.value(QETInformation::ELMT_LOCATION).toString();
}
/**
@brief labelOfEnd
@return the label (the BMK) of the piece the end lands on, empty for
an end of the drawn line -- there is no piece there to carry one
*/
QString labelOfEnd(const CableEnd &end)
{
if (!end.element) {
return QString();
}
return end.element->elementInformations()
.value(QETInformation::ELMT_LABEL).toString();
}
} // namespace
/**
@brief CableList::allColumns
@return every column of the cable list, in the order they are
offered, drawn and exported
*/
const QList<CableList::Column> &CableList::allColumns()
{
static const QList<Column> columns = {
{QStringLiteral("nr"), tr("N°")},
{QStringLiteral("installation_cable"), tr("Installation câble")},
{QStringLiteral("location_cable"), tr("Localisation câble")},
{QStringLiteral("bmk_cable"), tr("Label câble")},
{QStringLiteral("installation_start"), tr("Installation début")},
{QStringLiteral("location_start"), tr("Localisation début")},
{QStringLiteral("bmk_start"), tr("Label début")},
{QStringLiteral("folio_start"), tr("Folio début")},
{QStringLiteral("installation_end"), tr("Installation fin")},
{QStringLiteral("location_end"), tr("Localisation fin")},
{QStringLiteral("bmk_end"), tr("Label fin")},
{QStringLiteral("folio_end"), tr("Folio fin")},
{QStringLiteral("type"), tr("Type de câble")},
{QStringLiteral("length"), tr("Longueur")},
{QStringLiteral("cores_used"), tr("Âmes utilisées")},
};
return columns;
}
/**
@brief CableList::defaultKeys
@return the columns a freshly drawn cable list starts with: all of
them
*/
const QStringList &CableList::defaultKeys()
{
static const QStringList keys = []() {
QStringList list;
for (const Column &column : allColumns()) {
list << column.key;
}
return list;
}();
return keys;
}
/**
@brief CableList::labelOf
@param key
@return the header text of that column, empty when the key names no
column of this list
*/
QString CableList::labelOf(const QString &key)
{
for (const Column &column : allColumns()) {
if (column.key == key) {
return column.label;
}
}
return QString();
}
/**
@brief CableList::rows
Work out one row per cable, in plan order.
@param project the project whose cables are listed
@return the rows, sorted, the number column already filled in
*/
QVector<QMap<QString, QString>> CableList::rows(QETProject *project)
{
QVector<QMap<QString, QString>> result;
if (!project) {
return result;
}
//Every conductor of every cable is walked once, and where one
//lands on a real piece becomes an arrival of that cable. A core
//which ends on a folio report is skipped on that side: the
//cable goes on over there, and the landing on the far sheet is
//what counts as its end.
QHash<QUuid, QVector<CableEnd>> arrivals;
for (Diagram *diagram : project->diagrams())
{
for (Conductor *conductor : diagram->conductors())
{
const ConductorProperties properties = conductor->properties();
if (properties.m_cable_uuid.isNull()) {
continue;
}
if (!conductor->terminal1 || !conductor->terminal2) {
continue;
}
Terminal *terminals[2] = {conductor->terminal1, conductor->terminal2};
for (Terminal *terminal : terminals)
{
Element *element = terminal->parentElement();
if (!element) {
//a free end is not an arrival
continue;
}
if (element->linkType() & Element::AllReport) {
//the cable continues on the linked sheet
continue;
}
CableEnd end;
end.diagram = element->diagram();
end.scene = terminal->scenePos();
end.element = element;
end.element_uuid = element->uuid();
end.tie = conductor->uuid().toString();
arrivals[properties.m_cable_uuid].append(end);
}
}
}
struct Entry
{
Cable *cable = nullptr;
CableEnd start;
CableEnd target;
};
auto byPlan = [project](const CableEnd &a, const CableEnd &b) {
return endBefore(a, b, project);
};
QVector<Entry> entries;
entries.reserve(project->cables().count());
for (Cable *cable : project->cables())
{
//Several cores landing on one piece are one end: the highest
//of their landings speaks for it.
QHash<QUuid, CableEnd> grouped;
for (const CableEnd &end : arrivals.value(cable->uuid()))
{
auto it = grouped.find(end.element_uuid);
if (it == grouped.end() || byPlan(end, it.value())) {
grouped.insert(end.element_uuid, end);
}
}
QList<CableEnd> landings = grouped.values();
std::sort(landings.begin(), landings.end(), byPlan);
//The free ends of the drawn line, for the sides no core ever
//landed on
QList<CableEnd> line_ends;
for (const CablePartData &part : cable->parts())
{
Diagram *diagram = project->diagramByUuid(part.diagram);
if (!diagram) {
continue;
}
for (int i = 0; i < 2; ++i)
{
CableEnd end;
end.diagram = diagram;
end.scene = i ? part.p2 : part.p1;
end.tie = part.uuid.toString() + (i ? QStringLiteral("2")
: QStringLiteral("1"));
line_ends.append(end);
}
}
std::sort(line_ends.begin(), line_ends.end(), byPlan);
Entry entry;
entry.cable = cable;
if (landings.size() >= 2)
{
//The rule as asked for: the first finding counts for
//both ends. First landing in plan order is the start,
//the very next finding is the target -- not the last
//one. A sheet further along finds the same cable again
//but start and target are already taken by then, so it
//adds nothing: on the first sheet, the upper piece is
//the start and the lower one the target.
entry.start = landings.first();
entry.target = landings.at(1);
}
else if (landings.size() == 1)
{
//One side is wired: that side carries the label, the
//other side falls back to the drawn line, farthest end
//first so a tie goes to the later point of the plan.
entry.start = landings.first();
entry.target = landings.first();
qreal farthest = -1.0;
for (const CableEnd &candidate : std::as_const(line_ends))
{
const qreal distance = QLineF(entry.start.scene, candidate.scene).length();
if (distance >= farthest) {
farthest = distance;
entry.target = candidate;
}
}
}
else if (line_ends.size() >= 2)
{
entry.start = line_ends.first();
entry.target = line_ends.last();
}
else if (line_ends.size() == 1)
{
entry.start = line_ends.first();
entry.target = line_ends.first();
}
//A cable with neither a core nor a line draws no ends at all;
//its own fields still fill the row.
entries.append(entry);
}
//Plan order again for the rows themselves: by the sheet the start
//lies on, then from the top of that sheet down. The name and the
//uuid only ever break ties, so the numbering is stable between
//two openings of the same project.
std::sort(entries.begin(), entries.end(), [project](const Entry &a, const Entry &b) {
if (endBefore(a.start, b.start, project)) return true;
if (endBefore(b.start, a.start, project)) return false;
if (endBefore(a.target, b.target, project)) return true;
if (endBefore(b.target, a.target, project)) return false;
if (a.cable->designation() != b.cable->designation()) {
return a.cable->designation() < b.cable->designation();
}
return a.cable->uuid().toString() < b.cable->uuid().toString();
});
int number = 0;
for (const Entry &entry : std::as_const(entries))
{
Cable *cable = entry.cable;
QMap<QString, QString> row;
row.insert(QStringLiteral("nr"), QString::number(++number));
row.insert(QStringLiteral("installation_cable"), cable->installation());
row.insert(QStringLiteral("location_cable"), cable->location());
row.insert(QStringLiteral("bmk_cable"), cable->designation());
row.insert(QStringLiteral("installation_start"), plantOf(entry.start));
row.insert(QStringLiteral("location_start"), locmachOf(entry.start));
row.insert(QStringLiteral("bmk_start"), labelOfEnd(entry.start));
row.insert(QStringLiteral("folio_start"), folioOf(entry.start, project));
row.insert(QStringLiteral("installation_end"), plantOf(entry.target));
row.insert(QStringLiteral("location_end"), locmachOf(entry.target));
row.insert(QStringLiteral("bmk_end"), labelOfEnd(entry.target));
row.insert(QStringLiteral("folio_end"), folioOf(entry.target, project));
row.insert(QStringLiteral("type"), cable->type());
row.insert(QStringLiteral("length"), cable->length());
row.insert(QStringLiteral("cores_used"),
QStringLiteral("%1/%2")
.arg(cable->usedCoreCount())
.arg(cable->coreCount()));
result.append(row);
}
return result;
}
/**
@brief CableList::toCsv
The whole list as CSV bytes: every column of the list, every cable
of the project, in the format every CSV export of this program
writes.
@param project the project whose cables are listed
@return the bytes to write
*/
/**
@brief CableList::toCsv
@param project the project whose cables are listed
@param keys the columns to write, in that order; unknown keys are
dropped, and a list without a single usable one falls back to the
default columns rather than writing a blank file
@param include_headers whether the line of column names is written
as the first line
@return the CSV bytes in the format every CSV export of this
program writes (UTF-8 BOM, ';', every field quoted)
*/
QByteArray CableList::toCsv(QETProject *project, const QStringList &keys,
bool include_headers)
{
QStringList kept;
for (const QString &key : keys)
{
if (kept.contains(key) || labelOf(key).isEmpty()) {
continue;
}
kept << key;
}
if (kept.isEmpty()) {
kept = defaultKeys();
}
QByteArray csv("\xEF\xBB\xBF");
if (include_headers) {
QStringList headers;
for (const QString &key : std::as_const(kept)) {
headers << labelOf(key);
}
csv += BomExport::csvRecord(headers);
}
const QVector<QMap<QString, QString>> rows_ = rows(project);
for (const QMap<QString, QString> &row : rows_)
{
QStringList values;
for (const QString &key : std::as_const(kept)) {
values << row.value(key);
}
csv += BomExport::csvRecord(values);
}
return csv;
}
/**
@brief CableList::toCsv
The whole list as it has always been written: every column, every
cable, the line of column names included
@param project the project whose cables are listed
@return the CSV bytes
*/
QByteArray CableList::toCsv(QETProject *project)
{
return toCsv(project, defaultKeys(), true);
}
+101
View File
@@ -0,0 +1,101 @@
/*
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 3 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 CABLELISTROWS_H
#define CABLELISTROWS_H
#include <QByteArray>
#include <QCoreApplication>
#include <QMap>
#include <QString>
#include <QStringList>
#include <QVector>
class QETProject;
/**
@brief The CableList class
The columns and the rows of the cable list (menu "Listes": the entry
which puts such a table on a sheet, and the CSV export which writes
the same table to a file).
Everything which decides what a cable list row says lives here, so
that the table drawn on the sheet and the exported CSV can never
disagree: one row per cable, worked out once and read twice.
The two ends of a cable are worked out the way the user asked for
it:
- wherever the cores of the cable land on a real piece (a terminal
strip, a device) is an end. Several cores landing on the same
piece are one end. A core which ends on a folio report is not an
end at all -- the cable goes on over there, and the landing on the
far sheet is the end instead, which is what makes a cable running
over several sheets show its start on the earlier sheet and its
target on the later one;
- ends are ordered by sheet first (the earlier sheet wins), then by
height in the plan (the higher one wins), then left to right;
the first end is the start, the last one the target;
- a cable whose cores never landed anywhere (not drawn yet, or only
wired in the middle) falls back to the two free ends of its drawn
line; when there is only one landing, the other side falls back to
the line end farthest away from it.
*/
class CableList
{
Q_DECLARE_TR_FUNCTIONS(CableList)
public:
struct Column
{
/// The name the row hash, the saved file and the CSV use
QString key;
/// What the header over the column says
QString label;
};
/// Every column the list can offer, in the order they are
/// offered and exported
static const QList<Column> &allColumns();
/// The columns a fresh list starts with: all of them but the
/// ones marked optional above
static const QStringList &defaultKeys();
/// The header text of one column, empty when the key is not
/// a column of this list
static QString labelOf(const QString &key);
/// One row per cable, sorted: first by the sheet its start
/// lies on, then from the top of that sheet down, the number
/// column filled in along the way
static QVector<QMap<QString, QString>> rows(QETProject *project);
/// The whole list as CSV bytes, in the format every CSV
/// export of this program writes (UTF-8 BOM, ';', every
/// field quoted): every column, every cable, the line of
/// column names included
static QByteArray toCsv(QETProject *project);
/// The same, but only the given columns, in the given
/// order, and only with the line of column names when asked
/// for -- what the export dialog writes. Unknown keys are
/// dropped, and no usable key at all falls back to the
/// default columns
static QByteArray toCsv(QETProject *project, const QStringList &keys,
bool include_headers);
};
#endif // CABLELISTROWS_H
+319
View File
@@ -0,0 +1,319 @@
/*
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 3 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 "cablequerywidget.h"
#include "cablelistrows.h"
#include "../qetapp.h"
#include "../ui/configsaveloaderwidget.h"
#include <QFile>
#include <QHBoxLayout>
#include <QJsonDocument>
#include <QJsonObject>
#include <QLabel>
#include <QListWidget>
#include <QListWidgetItem>
#include <QPushButton>
#include <QStyle>
#include <QVBoxLayout>
#include <QStringBuilder>
/**
@brief CableQueryWidget::CableQueryWidget
@param parent
*/
CableQueryWidget::CableQueryWidget(QWidget *parent) :
QWidget(parent)
{
auto *layout = new QVBoxLayout(this);
auto *lists = new QHBoxLayout;
auto *left = new QVBoxLayout;
left->addWidget(new QLabel(tr("Colonnes disponibles"), this));
m_available_list = new QListWidget(this);
left->addWidget(m_available_list);
auto *buttons = new QVBoxLayout;
buttons->addStretch();
auto make_button = [this](QStyle::StandardPixmap icon, const QString &tip) {
auto *button = new QPushButton(this);
button->setIcon(style()->standardIcon(icon));
button->setToolTip(tip);
return button;
};
auto *add_pb = make_button(QStyle::SP_ArrowRight, tr("Ajouter la colonne sélectionnée"));
auto *remove_pb = make_button(QStyle::SP_ArrowLeft, tr("Retirer la colonne sélectionnée"));
auto *up_pb = make_button(QStyle::SP_ArrowUp, tr("Monter la colonne sélectionnée"));
auto *down_pb = make_button(QStyle::SP_ArrowDown, tr("Descendre la colonne sélectionnée"));
buttons->addWidget(add_pb);
buttons->addWidget(remove_pb);
buttons->addWidget(up_pb);
buttons->addWidget(down_pb);
buttons->addStretch();
auto *right = new QVBoxLayout;
right->addWidget(new QLabel(tr("Colonnes du tableau"), this));
m_choosen_list = new QListWidget(this);
right->addWidget(m_choosen_list);
lists->addLayout(left);
lists->addLayout(buttons);
lists->addLayout(right);
layout->addLayout(lists);
m_config_gb = new ConfigSaveLoaderWidget(this);
layout->addWidget(m_config_gb);
setMinimumSize(QSize(520, 420));
setUpItems();
fillSavedQuery();
connect(m_config_gb, &ConfigSaveLoaderWidget::saveClicked, this, &CableQueryWidget::saveConfig);
connect(m_config_gb, &ConfigSaveLoaderWidget::loadClicked, this, &CableQueryWidget::loadConfig);
connect(add_pb, &QPushButton::clicked, this, &CableQueryWidget::addField);
connect(remove_pb, &QPushButton::clicked, this, &CableQueryWidget::removeField);
connect(up_pb, &QPushButton::clicked, this, &CableQueryWidget::moveFieldUp);
connect(down_pb, &QPushButton::clicked, this, &CableQueryWidget::moveFieldDown);
connect(m_available_list, &QListWidget::itemDoubleClicked,
this, &CableQueryWidget::on_m_available_list_itemDoubleClicked);
connect(m_choosen_list, &QListWidget::itemDoubleClicked,
this, &CableQueryWidget::on_m_choosen_list_itemDoubleClicked);
}
/**
@brief CableQueryWidget::~CableQueryWidget
*/
CableQueryWidget::~CableQueryWidget()
{
}
/**
@brief CableQueryWidget::selectedKeys
@return the chosen columns, in order
*/
QStringList CableQueryWidget::selectedKeys() const
{
QStringList keys;
int row = 0;
while (auto *item = m_choosen_list->item(row))
{
keys.append(item->data(Qt::UserRole).toString());
++row;
}
return keys;
}
/**
@brief CableQueryWidget::setFields
Put exactly these columns in the right list, in that order. Keys
this list does not know are ignored, which is what keeps a file
coming from a newer program readable here.
@param fields
*/
void CableQueryWidget::setFields(const QStringList &fields)
{
//Ugly hack to force to remove all chosen columns, the same way
//SummaryQueryWidget::reset() does it
while (auto *item = m_choosen_list->takeItem(0)) {
m_available_list->addItem(item);
}
for (const QString &key : fields)
{
for (int row = 0; row < m_available_list->count(); ++row)
{
QListWidgetItem *item = m_available_list->item(row);
if (item->data(Qt::UserRole).toString() == key) {
m_available_list->takeItem(row);
m_choosen_list->addItem(item);
break;
}
}
}
}
/**
@brief CableQueryWidget::setUpItems
Fill the available list with every column the list offers and
leave them there: a fresh dialog opens with its entries on the
left, the same way the material list dialog does. What a table or
an export starts with when nothing has been picked is the default
set of columns, applied where the table or the file is made.
*/
void CableQueryWidget::setUpItems()
{
for (const CableList::Column &column : CableList::allColumns())
{
auto *item = new QListWidgetItem(column.label, m_available_list);
item->setData(Qt::UserRole, column.key);
}
}
/**
@brief CableQueryWidget::fillSavedQuery
Fill the combo box of the saved configurations
*/
void CableQueryWidget::fillSavedQuery()
{
QFile file(QETApp::configDir() % QStringLiteral("/cable_list.json"));
if (file.open(QFile::ReadOnly))
{
QJsonDocument jsd(QJsonDocument::fromJson(file.readAll()));
QJsonObject jso = jsd.object();
for (auto it = jso.begin() ; it != jso.end() ; ++it) {
m_config_gb->addItem(it.key());
}
}
}
/**
@brief CableQueryWidget::on_m_available_list_itemDoubleClicked
@param item
*/
void CableQueryWidget::on_m_available_list_itemDoubleClicked(QListWidgetItem *item)
{
Q_UNUSED(item)
addField();
}
/**
@brief CableQueryWidget::on_m_choosen_list_itemDoubleClicked
@param item
*/
void CableQueryWidget::on_m_choosen_list_itemDoubleClicked(QListWidgetItem *item)
{
Q_UNUSED(item)
removeField();
}
/**
@brief CableQueryWidget::addField
*/
void CableQueryWidget::addField()
{
if (auto *item = m_available_list->takeItem(m_available_list->currentRow())) {
m_choosen_list->addItem(item);
}
}
/**
@brief CableQueryWidget::removeField
*/
void CableQueryWidget::removeField()
{
if (auto *item = m_choosen_list->takeItem(m_choosen_list->currentRow())) {
m_available_list->addItem(item);
}
}
/**
@brief CableQueryWidget::moveFieldUp
*/
void CableQueryWidget::moveFieldUp()
{
auto row = m_choosen_list->currentRow();
if (row <= 0) {
return;
}
auto *item = m_choosen_list->takeItem(row);
m_choosen_list->insertItem(row - 1, item);
m_choosen_list->setCurrentItem(item);
}
/**
@brief CableQueryWidget::moveFieldDown
*/
void CableQueryWidget::moveFieldDown()
{
auto row = m_choosen_list->currentRow();
if (row == -1) {
return;
}
auto *item = m_choosen_list->takeItem(row);
m_choosen_list->insertItem(row + 1, item);
m_choosen_list->setCurrentItem(item);
}
/**
@brief CableQueryWidget::saveConfig
*/
void CableQueryWidget::saveConfig()
{
QFile file_(QETApp::configDir() % QStringLiteral("/cable_list.json"));
if (file_.open(QFile::ReadWrite))
{
QJsonDocument doc_(QJsonDocument::fromJson(file_.readAll()));
QJsonObject root_object;
if (!doc_.isEmpty())
{
root_object = doc_.object();
if (root_object.contains(m_config_gb->text())) {
root_object.remove(m_config_gb->text());
}
}
QJsonObject object_;
object_.insert(QStringLiteral("fields"), selectedKeys().join(QLatin1Char(',')));
root_object[m_config_gb->text()] = object_;
doc_.setObject(root_object);
file_.resize(0);
file_.write(doc_.toJson());
}
}
/**
@brief CableQueryWidget::loadConfig
*/
void CableQueryWidget::loadConfig()
{
auto name = m_config_gb->selectedText();
if (name.isEmpty()) {
return;
}
QFile file_(QETApp::configDir() % QStringLiteral("/cable_list.json"));
if (!file_.open(QFile::ReadOnly)) {
return;
}
QJsonDocument doc_(QJsonDocument::fromJson(file_.readAll()));
QJsonObject object_ = doc_.object();
auto value = object_.value(name);
if (!value.isObject()) {
return;
}
auto fields = value.toObject().value(QStringLiteral("fields"));
if (fields.isString()) {
setFields(fields.toString().split(QLatin1Char(','), Qt::SkipEmptyParts));
}
}
+71
View File
@@ -0,0 +1,71 @@
/*
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 3 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 CABLEQUERYWIDGET_H
#define CABLEQUERYWIDGET_H
#include <QWidget>
class ConfigSaveLoaderWidget;
class QListWidget;
class QListWidgetItem;
class QPushButton;
/**
@brief The CableQueryWidget class
The content tab of the dialog which puts a cable list on a sheet:
which columns the list shows, and in which order. Shaped after
SummaryQueryWidget -- two lists with arrows between them, a saved
configuration per name -- but the columns are chosen out of the
fixed set of CableList instead of out of a SQL query, so there is no
query line to edit.
*/
class CableQueryWidget : public QWidget
{
Q_OBJECT
public:
explicit CableQueryWidget(QWidget *parent = nullptr);
~CableQueryWidget();
static QString modelIdentifier() {return "cable_list";}
/// The chosen columns, in order
QStringList selectedKeys() const;
/// Put exactly these columns in the right list, in that
/// order; keys this list does not know are ignored
void setFields(const QStringList &fields);
private slots:
void on_m_available_list_itemDoubleClicked(QListWidgetItem *item);
void on_m_choosen_list_itemDoubleClicked(QListWidgetItem *item);
void addField();
void removeField();
void moveFieldUp();
void moveFieldDown();
void saveConfig();
void loadConfig();
private:
void setUpItems();
void fillSavedQuery();
QListWidget *m_available_list = nullptr;
QListWidget *m_choosen_list = nullptr;
ConfigSaveLoaderWidget *m_config_gb = nullptr;
};
#endif // CABLEQUERYWIDGET_H
+790
View File
@@ -0,0 +1,790 @@
/*
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 "cabletypelist.h"
#include "../materiallist/materiallist.h"
#include "../qetinformation.h"
#include <QCoreApplication>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QSaveFile>
#include <QSettings>
#include <QStandardPaths>
#include <utility>
namespace {
/**
@brief The columns of a cable type file -- the whole catalogue, and
the only ones the program reads anything from.
Three of them: the name of the type, how many cores it has and the
colour of each of them. In that order they are written into a brand
new file, and a label line is matched against them so that a
catalogue read back never depends on the wording of its first line.
The material file the pattern was copied from holds more columns
(section, sheath colour, manufacturer and so on); they were never
read here and are not listed. A file which has them anyway loses
nothing: a header cell outside this list keeps its own name and its
own data, it carries no meaning, and the machine header line is
recognized by its shape rather than by this list, so an extra
column there never turns that line into a record of cables.
*/
const QStringList &canonicalColumns()
{
static const QStringList columns = {
QStringLiteral("designation"),
QStringLiteral("cores"),
QStringLiteral("core_colors")
};
return columns;
}
/**
@brief Tell whether a cell of the machine header can be a column key.
A key is one plain lowercase word -- core_colors and
manufacturer_reference are typical -- written the same whichever
language the file is read in, which is exactly why it can sit under
the label line. A key the program doesn't know is still a key : its
column is kept and shown the way it is written, it carries no
meaning. What the shape really protects is the other side: a cable
("H07V-K 3G1,5") never looks like this, so a line of cables is
never mistaken for a header.
@param cell
@return true when the cell can be part of the machine header line
*/
bool isMachineKey(const QString &cell)
{
if (cell.isEmpty()) {
return false;
}
for (const QChar c : cell)
{
const ushort u = c.unicode();
const bool key = (u >= 'a' && u <= 'z')
|| (u >= '0' && u <= '9')
|| u == '_';
if (!key) {
return false;
}
}
//A key starts with a letter, so that a row of numbers can never
//pass for one.
return cell.at(0).unicode() >= 'a' && cell.at(0).unicode() <= 'z';
}
/**
@brief Decode a csv file content.
The cable type file is written in utf-8 with a BOM, the same way the
material file is, so spreadsheets open it with the right encoding. A
file written by hand in the local encoding is still decodable:
fromUtf8() is only used when the bytes are valid utf-8.
@param raw
@return
*/
QString decode(const QByteArray &raw)
{
if (raw.startsWith("\xEF\xBB\xBF")) {
return QString::fromUtf8(raw.constData() + 3, raw.size() - 3);
}
const QByteArray utf8 = QString::fromUtf8(raw).toUtf8();
if (utf8 == raw) {
return QString::fromUtf8(raw);
}
return QString::fromLocal8Bit(raw);
}
/**
@brief Tell whether a file exists and holds something.
An existing but empty file is the same as no file at all: it can be
filled with the template without losing anything.
@param path
@return
*/
bool hasContent(const QString &path)
{
const QFileInfo info(path);
return info.exists() && info.isFile() && info.size() > 0;
}
} // namespace
/**
@brief CableTypeList::settingsKey
@return the QSettings key holding the path of the cable type file
*/
QString CableTypeList::settingsKey()
{
return QStringLiteral("cable-management/cable-type-list-path");
}
/**
@brief CableTypeList::configuredPath
@return the path of the cable type file as chosen in the preferences,
or a null QString when the user never set one.
*/
QString CableTypeList::configuredPath()
{
QSettings settings;
const QString path = settings.value(settingsKey(), QStringLiteral("default")).toString();
if (path.isEmpty() || path == QLatin1String("default")) {
return QString();
}
return path;
}
/**
@brief CableTypeList::setConfiguredPath
Store the path of the cable type file in the preferences.
@param path
*/
void CableTypeList::setConfiguredPath(const QString &path)
{
QSettings settings;
settings.setValue(settingsKey(), path.isEmpty() ? QStringLiteral("default") : path);
}
/**
@brief CableTypeList::defaultFileName
@return the file name used when the cable type file is created without
an explicit location.
*/
QString CableTypeList::defaultFileName()
{
return QStringLiteral("qet_cable_types.csv");
}
/**
@brief CableTypeList::defaultPath
@return where the cable type file is created when no path is
configured yet : the standard documents folder.
*/
QString CableTypeList::defaultPath()
{
QString dir = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
while (dir.endsWith(QLatin1Char('/'))) {
dir.chop(1);
}
if (dir.isEmpty()) {
dir = QDir::currentPath();
}
return dir + QLatin1Char('/') + defaultFileName();
}
/**
@brief CableTypeList::savedHeaderState
@return the saved column layout of the selection window, empty when the
window was never opened or the layout could not be read back.
*/
QByteArray CableTypeList::savedHeaderState()
{
QSettings settings;
return settings.value(QStringLiteral("cable-management/cable-type-list-header")).toByteArray();
}
/**
@brief CableTypeList::saveHeaderState
Remember the column layout of the selection window.
@param state the header state as saved by QHeaderView::saveState()
*/
void CableTypeList::saveHeaderState(const QByteArray &state)
{
QSettings settings;
settings.setValue(QStringLiteral("cable-management/cable-type-list-header"), state);
}
/**
@brief CableTypeList::savedDialogSize
@return the size the selection window had the last time it was closed,
an invalid size when it was never opened.
*/
QSize CableTypeList::savedDialogSize()
{
QSettings settings;
const QSize size(settings.value(QStringLiteral("cable-management/cable-type-list-width")).toInt(),
settings.value(QStringLiteral("cable-management/cable-type-list-height")).toInt());
//A size which was never written reads back as 0x0, which Qt still
//counts as valid : it must not be handed to resize().
return size.isEmpty() ? QSize() : size;
}
/**
@brief CableTypeList::saveDialogSize
Remember the size of the selection window.
@param size
*/
void CableTypeList::saveDialogSize(const QSize &size)
{
if (size.isEmpty()) {
return;
}
QSettings settings;
settings.setValue(QStringLiteral("cable-management/cable-type-list-width"), size.width());
settings.setValue(QStringLiteral("cable-management/cable-type-list-height"), size.height());
}
/**
@brief CableTypeList::defaultColumns
@return the columns of a brand new cable type file, in writing order.
*/
QStringList CableTypeList::defaultColumns()
{
return canonicalColumns();
}
/**
@brief CableTypeList::translatedColumn
@param column a column of the cable type file
@return that column named the way it is shown to the user, in the
current language. A column QElectroTech doesn't know is returned as it
is, since it can only have been named by the user.
*/
QString CableTypeList::translatedColumn(const QString &column)
{
//The columns which are specific to a cable keep their own wording,
//the ones shared with the material file reuse the element
//information wording, so both catalogues read the same. The type
//column is the exception: it is called Désignation here, the way
//the cable form calls it, and not "Numéro d'article", which is
//what an element means by it.
static const QMap<QString, QString> own = {
{QStringLiteral("designation"),
QCoreApplication::translate("CableTypeList", "Désignation")},
{QStringLiteral("cores"),
QCoreApplication::translate("CableTypeList", "Nombre d'âmes")},
{QStringLiteral("core_colors"),
QCoreApplication::translate("CableTypeList", "Couleurs des âmes")},
{QStringLiteral("section"),
QCoreApplication::translate("CableTypeList", "Section du câble")},
{QStringLiteral("color"),
QCoreApplication::translate("CableTypeList", "Couleur du fourreau")}
};
const auto it = own.constFind(column);
if (it != own.constEnd()) {
return it.value();
}
const QString translated = QETInformation::translatedInfoKey(column);
return translated.isEmpty() ? column : translated;
}
/**
@brief CableTypeList::translatedHeader
The label line of a cable type file : the columns the way the user
reads them, in the current language.
@param columns
@return
*/
QStringList CableTypeList::translatedHeader(const QStringList &columns)
{
QStringList header;
header.reserve(columns.size());
for (const QString &column : columns) {
header.append(translatedColumn(column));
}
return header;
}
/**
@brief CableTypeList::designation
@param record
@return the type name of that cable, the way it shows in the
selection list and on the drawing
*/
QString CableTypeList::designation(const CableTypeRecord &record)
{
return record.value(QStringLiteral("designation"));
}
/**
@brief CableTypeList::coreCount
Tell how many cores a cable type holds.
@param record
@return the number of cores, or 0 when the file doesn't say. 0 means
"unknown", which the selection window treats as usable : a file which
says nothing is no reason to refuse a type.
*/
int CableTypeList::coreCount(const CableTypeRecord &record)
{
bool ok = false;
const int cores = record.value(QStringLiteral("cores")).trimmed().toInt(&ok);
return (ok && cores > 0) ? cores : 0;
}
/**
@brief CableTypeList::coreColors
The colours of the cores, in the order they are written in the file,
which is the order they are given to the cores of a drawn cable.
@param record
@return
The cell holds the colours separated by |, ; or ,. Writing always
uses | because it can't appear in a colour name and never needs
quoting, reading accepts the three so a hand written file works too.
*/
QStringList CableTypeList::coreColors(const CableTypeRecord &record)
{
QStringList colors;
const QString cell = record.value(QStringLiteral("core_colors"));
for (const QString &raw : cell.split(QLatin1Char('|'), Qt::SkipEmptyParts))
{
for (const QString &part : raw.split(QLatin1Char(';'), Qt::SkipEmptyParts))
{
for (const QString &color : part.split(QLatin1Char(','), Qt::SkipEmptyParts))
{
const QString trimmed = color.trimmed();
if (!trimmed.isEmpty()) {
colors.append(trimmed);
}
}
}
}
return colors;
}
/**
@brief CableTypeList::headerAliasMap
@return every accepted header cell of a cable type file, lowercased,
mapped to the canonical column it means.
Beside the key itself, a header may use the translated name of the
column ("Nombre d'âmes" for cores), or the name and translation of an
element information, so a catalogue sharing columns with the material
file is understood in both.
*/
QMap<QString, QString> CableTypeList::headerAliasMap()
{
QMap<QString, QString> map;
for (const QString &key : canonicalColumns())
{
map.insert(key.toLower(), key);
const QString translated = translatedColumn(key);
if (!translated.isEmpty()) {
map.insert(translated.toLower(), key);
}
//The element information spells the same column another way
//("Numéro d'article" for designation); both are accepted, so
//that a file written with either wording reads the same.
const QString info_key = QETInformation::translatedInfoKey(key);
if (!info_key.isEmpty() && info_key != translated) {
map.insert(info_key.toLower(), key);
}
}
//Every element information is an acceptable column name, so that
//columns copied over from the material file keep their meaning.
const QStringList keys = QETInformation::elementInfoKeys();
for (const QString &key : keys)
{
if (map.contains(key.toLower())) {
continue;
}
map.insert(key.toLower(), key);
const QString translated = QETInformation::translatedInfoKey(key);
if (!translated.isEmpty()) {
map.insert(translated.toLower(), key);
}
}
return map;
}
/**
@brief CableTypeList::canonicalColumn
Turn one header cell into a canonical column name.
@param header_cell
@param already_used the names chosen for the previous cells, to keep
the header free of duplicates
@param alias_map lowercased alias -> canonical name
@return
*/
QString CableTypeList::canonicalColumn(const QString &header_cell,
const QStringList &already_used,
const QMap<QString, QString> &alias_map)
{
const QString raw = header_cell.trimmed();
if (raw.isEmpty()) {
//A nameless column keeps its data and its place, it just can't be
//matched to anything.
QString name = QStringLiteral("column_%1").arg(already_used.size() + 1);
int i = already_used.size() + 1;
while (already_used.contains(name)) {
name = QStringLiteral("column_%1").arg(++i);
}
return name;
}
const auto it = alias_map.constFind(raw.toLower());
if (it != alias_map.constEnd() && !already_used.contains(it.value())) {
return it.value();
}
//Either an unknown name, or a second cell meaning something which is
//already used : the cell is kept as it is written, its data is never
//thrown away.
QString name = raw;
int i = 2;
while (already_used.contains(name)) {
name = raw + QStringLiteral("_%1").arg(i++);
}
return name;
}
/**
@brief CableTypeList::isMachineHeaderLine
Tell whether the second line of a cable type file is the machine
header QElectroTech writes under the label line : the canonical
column names, kept so that a file written in one language is still
understood in another one.
@param label_line the first line of the file, the one the user reads
@param candidate the line which may be the machine header
@param alias_map lowercased alias -> canonical name
@return true when candidate must not be read as a record
*/
bool CableTypeList::isMachineHeaderLine(const QStringList &label_line,
const QStringList &candidate,
const QMap<QString, QString> &alias_map)
{
if (candidate.isEmpty() || candidate.size() != label_line.size()) {
return false;
}
//At least one cell the program really knows : a line made only
//of key shaped words nobody here gave a meaning to is far more
//likely to be a cable than a header.
bool has_known_column = false;
for (int i = 0; i < candidate.size(); ++i)
{
const QString cell = candidate.at(i).trimmed();
if (cell.isEmpty()) {
continue;
}
if (alias_map.contains(cell.toLower()))
{
has_known_column = true;
continue;
}
if (cell == label_line.at(i).trimmed()) {
continue;
}
//An unknown key is still a key : the column comes back as
//it was written, its data is never thrown away.
if (isMachineKey(cell)) {
continue;
}
return false;
}
return has_known_column;
}
/**
@brief CableTypeList::load
Read a cable type file.
@param path
@param data receives the columns and the records
@param error receives a message when the file can't be read
@return true on success
*/
bool CableTypeList::load(const QString &path, CableTypeListData *data, QString *error)
{
if (error) {
error->clear();
}
if (!data) {
if (error) {
*error = QCoreApplication::translate("CableTypeList", "Aucun récepteur pour le fichier des types de câbles.");
}
return false;
}
data->columns.clear();
data->records.clear();
data->separator = QLatin1Char(';');
QFile file(path);
if (!file.open(QIODevice::ReadOnly))
{
if (error) {
*error = file.errorString();
}
return false;
}
const QByteArray raw = file.readAll();
file.close();
const QString content = decode(raw);
if (content.trimmed().isEmpty()) {
return true;
}
const int eol = content.indexOf(QLatin1Char('\n'));
data->separator = MaterialList::detectSeparator(eol < 0 ? content : content.left(eol));
QList<QStringList> rows = MaterialList::parseCsv(content, data->separator);
if (rows.isEmpty()) {
return true;
}
const QStringList header = rows.takeFirst();
const QMap<QString, QString> alias_map = headerAliasMap();
//The label line is followed by the machine header QElectroTech
//writes itself : the canonical names, so that the file is read
//the same way whatever the language it was written in.
QStringList machine_line;
if (!rows.isEmpty() && isMachineHeaderLine(header, rows.first(), alias_map)) {
machine_line = rows.takeFirst();
}
QStringList used;
for (int i = 0; i < header.size(); ++i)
{
QString cell = header.at(i);
if (i < machine_line.size() && !machine_line.at(i).isEmpty()) {
cell = machine_line.at(i);
}
const QString column = canonicalColumn(cell, used, alias_map);
used.append(column);
data->columns.append(column);
}
for (const QStringList &row : std::as_const(rows))
{
CableTypeRecord record;
bool empty = true;
for (int i = 0; i < data->columns.size(); ++i)
{
const QString value = i < row.size() ? row.at(i) : QString();
if (!value.isEmpty()) {
empty = false;
}
record.setValue(data->columns.at(i), value);
}
//Cells written past the header are not thrown away : they are
//written back the way they were read.
for (int i = data->columns.size(); i < row.size(); ++i)
{
if (!row.at(i).isEmpty()) {
empty = false;
}
record.extra.append(row.at(i));
}
if (!empty) {
data->records.append(record);
}
}
return true;
}
/**
@brief CableTypeList::writeFile
Write a whole cable type file atomically : a crash or a full disk
never leaves a half written catalogue behind.
@param path
@param data
@param error receives a message when the file can't be written
@return true on success
*/
bool CableTypeList::writeFile(const QString &path, const CableTypeListData &data, QString *error)
{
if (error) {
error->clear();
}
QList<QStringList> rows;
rows.reserve(data.records.size());
for (const CableTypeRecord &record : data.records)
{
QStringList row;
row.reserve(data.columns.size());
for (const QString &column : data.columns) {
row.append(record.value(column));
}
row.append(record.extra);
rows.append(row);
}
QList<QStringList> header_lines;
header_lines.append(translatedHeader(data.columns));
header_lines.append(data.columns);
const QByteArray content = MaterialList::serializeCsv(header_lines, rows, data.separator);
QSaveFile file(path);
if (!file.open(QIODevice::WriteOnly) || file.write(content) != content.size())
{
if (error) {
*error = file.errorString();
}
file.cancelWriting();
return false;
}
if (!file.commit())
{
if (error) {
*error = file.errorString();
}
return false;
}
return true;
}
/**
@brief CableTypeList::appendRecord
Add one cable type at the end of a cable type file.
The file is read again first: changes the user just saved from his
spreadsheet are part of what gets rewritten, never dropped.
@param path
@param record
@param error receives a message when the file can't be written
@return true on success
*/
bool CableTypeList::appendRecord(const QString &path, const CableTypeRecord &record, QString *error)
{
if (error) {
error->clear();
}
CableTypeListData data;
if (hasContent(path))
{
if (!load(path, &data, error)) {
return false;
}
if (data.columns.isEmpty())
{
if (error) {
*error = QCoreApplication::translate("CableTypeList", "Le fichier ne contient pas d'en-tête : colonnes manquantes.");
}
return false;
}
}
else
{
data.columns = defaultColumns();
data.separator = QLatin1Char(';');
}
data.records.append(record);
return writeFile(path, data, error);
}
/**
@brief CableTypeList::updateRecord
Rewrite one line of a cable type file, the way a spreadsheet saves a
row he just edited: the line is found as it stands at this moment,
replaced by what the user left, and the whole file is written again
-- so the changes he saved from his spreadsheet in the meantime are
part of it rather than being thrown away.
@param path the file to write
@param before the line as it stands in the file right now
@param after the line as the user left it
@param error receives a message when the file can't be written, or
when the line he started from is no longer in it
@return true on success
*/
bool CableTypeList::updateRecord(const QString &path,
const CableTypeRecord &before,
const CableTypeRecord &after,
QString *error)
{
if (error) {
error->clear();
}
CableTypeListData data;
if (!load(path, &data, error)) {
return false;
}
if (data.columns.isEmpty())
{
if (error) {
*error = QCoreApplication::translate("CableTypeList", "Le fichier ne contient pas d'en-tête : colonnes manquantes.");
}
return false;
}
int row = -1;
for (int i = 0; i < data.records.size(); ++i)
{
if (data.records.at(i) == before) {
row = i;
break;
}
}
if (row < 0)
{
//The line he started from is gone: rewriting whatever stands
//there now would overwrite somebody else's work.
if (error) {
*error = QCoreApplication::translate("CableTypeList",
"Cette entrée n'existe plus dans le fichier : elle a peut-être été modifiée entre-temps.");
}
return false;
}
data.records[row] = after;
return writeFile(path, data, error);
}
/**
@brief CableTypeList::createFile
Create an empty cable type file holding only the header lines.
@param path
@param error receives a message when the file can't be written
@return true on success
*/
bool CableTypeList::createFile(const QString &path, QString *error)
{
if (error) {
error->clear();
}
if (hasContent(path))
{
if (error) {
*error = QCoreApplication::translate("CableTypeList", "Le fichier existe déjà et n'est pas vide.");
}
return false;
}
CableTypeListData data;
data.columns = defaultColumns();
data.separator = QLatin1Char(';');
return writeFile(path, data, error);
}
/**
@brief CableTypeList::isEmptyFile
@param path
@return true when no file, or an empty one, exists at path
*/
bool CableTypeList::isEmptyFile(const QString &path)
{
return !hasContent(path);
}
+134
View File
@@ -0,0 +1,134 @@
/*
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 CABLETYPELIST_H
#define CABLETYPELIST_H
#include <QByteArray>
#include <QChar>
#include <QList>
#include <QMap>
#include <QSize>
#include <QString>
#include <QStringList>
/**
@brief One line of the cable type file.
Same shape as MaterialRecord : the keys are column names, either one
QElectroTech knows (designation, manufacturer...) or one the user
invented, which is never interpreted but never thrown away either.
*/
struct CableTypeRecord
{
QMap<QString, QString> values;
//Cells written after the last column of the header, kept as they
//are, exactly like MaterialRecord::extra.
QStringList extra;
QString value(const QString &column) const {return values.value(column);}
void setValue(const QString &column, const QString &value) {values.insert(column, value);}
bool operator==(const CableTypeRecord &other) const {return values == other.values;}
};
/**
@brief The whole content of a cable type file.
The separator and the column names are read from the file rather than
imposed, so a catalogue written in a spreadsheet keeps working.
*/
struct CableTypeListData
{
QStringList columns;
QList<CableTypeRecord> records;
QChar separator = QLatin1Char(';');
bool isEmpty() const {return columns.isEmpty() && records.isEmpty();}
};
/**
@brief The CableTypeList class reads and writes the cable type file (a
CSV file listing the cable kinds a project may use : their name,
how many cores they have and the colour of each of them).
It is the cable counterpart of MaterialList and follows it step by
step : same settings handling, same two header lines (the labels the
user reads, then the canonical names QElectroTech matches against),
same lenient reading of a hand written file.
This is a pure data class : it knows nothing about widgets, so it
stays usable from the tests and from the command line.
*/
class CableTypeList
{
public:
//Settings
static QString settingsKey();
static QString configuredPath();
static void setConfiguredPath(const QString &path);
static QString defaultPath();
static QString defaultFileName();
//Geometry of the selection window, remembered from one opening
//to the next, the same way MaterialList remembers its own.
static QByteArray savedHeaderState();
static void saveHeaderState(const QByteArray &state);
static QSize savedDialogSize();
static void saveDialogSize(const QSize &size);
//Columns
static QStringList defaultColumns();
static QString translatedColumn(const QString &column);
static QStringList translatedHeader(const QStringList &columns);
//Cable semantics
static QString designation(const CableTypeRecord &record);
static int coreCount(const CableTypeRecord &record);
static QStringList coreColors(const CableTypeRecord &record);
//File io
static bool load(const QString &path, CableTypeListData *data, QString *error = nullptr);
static bool writeFile(const QString &path, const CableTypeListData &data, QString *error = nullptr);
static bool appendRecord(const QString &path, const CableTypeRecord &record, QString *error = nullptr);
/**
@brief Rewrite one line of a cable type file, the way a
spreadsheet saves a row he just edited.
@param path the file to write
@param before the line as it stands in the file right now
@param after the line as the user left it
@param error receives a message when the file can't be
written, or when that line is no longer in it
@return true on success
*/
static bool updateRecord(const QString &path,
const CableTypeRecord &before,
const CableTypeRecord &after,
QString *error = nullptr);
static bool createFile(const QString &path, QString *error = nullptr);
static bool isEmptyFile(const QString &path);
private:
static QMap<QString, QString> headerAliasMap();
static QString canonicalColumn(const QString &header_cell,
const QStringList &already_used,
const QMap<QString, QString> &alias_map);
static bool isMachineHeaderLine(const QStringList &label_line,
const QStringList &candidate,
const QMap<QString, QString> &alias_map);
};
#endif // CABLETYPELIST_H