mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-11 06:34:14 +02:00
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:
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user