mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-25 00:14:15 +02:00
Reapply "Add cable management: drawing, cores, numbering, cross-references and cable list"
This reverts commit 2bb3343d2d.
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("Export the cable list as 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("Layout"), this);
|
||||
auto *page_layout = new QVBoxLayout(page_group);
|
||||
m_include_headers = new QCheckBox(tr("include headers"), 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("Preview"), 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("cable_list_") + m_project->title()
|
||||
+ QStringLiteral(".csv");
|
||||
const QString file_path = QFileDialog::getSaveFileName(
|
||||
this, tr("Save As... "), file_name,
|
||||
tr("CSV files (*.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("Error"),
|
||||
tr("Cannot save the cable list to %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("Edit columns..."), this);
|
||||
m_refresh_pb = new QPushButton(tr("Refresh"), 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("Cable list columns"));
|
||||
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("No.")},
|
||||
{QStringLiteral("installation_cable"), tr("Plant (cable)")},
|
||||
{QStringLiteral("location_cable"), tr("Location (cable)")},
|
||||
{QStringLiteral("bmk_cable"), tr("Label (cable)")},
|
||||
{QStringLiteral("installation_start"), tr("Plant (start)")},
|
||||
{QStringLiteral("location_start"), tr("Location (start)")},
|
||||
{QStringLiteral("bmk_start"), tr("Label (start)")},
|
||||
{QStringLiteral("folio_start"), tr("Sheet (start)")},
|
||||
{QStringLiteral("installation_end"), tr("Plant (end)")},
|
||||
{QStringLiteral("location_end"), tr("Location (end)")},
|
||||
{QStringLiteral("bmk_end"), tr("Label (end)")},
|
||||
{QStringLiteral("folio_end"), tr("Sheet (end)")},
|
||||
{QStringLiteral("type"), tr("Cable type")},
|
||||
{QStringLiteral("length"), tr("Length")},
|
||||
{QStringLiteral("cores_used"), tr("Used cores")},
|
||||
};
|
||||
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("Available columns"), 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("Add the selected column"));
|
||||
auto *remove_pb = make_button(QStyle::SP_ArrowLeft, tr("Remove the selected column"));
|
||||
auto *up_pb = make_button(QStyle::SP_ArrowUp, tr("Move the selected column up"));
|
||||
auto *down_pb = make_button(QStyle::SP_ArrowDown, tr("Move the selected column down"));
|
||||
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("Table columns"), 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,824 @@
|
||||
/*
|
||||
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 Designation here, the way
|
||||
//the cable form calls it, and not "Article number", which is
|
||||
//what an element means by it.
|
||||
static const QMap<QString, QString> own = {
|
||||
{QStringLiteral("designation"),
|
||||
QCoreApplication::translate("CableTypeList", "Designation")},
|
||||
{QStringLiteral("cores"),
|
||||
QCoreApplication::translate("CableTypeList", "Number of cores")},
|
||||
{QStringLiteral("core_colors"),
|
||||
QCoreApplication::translate("CableTypeList", "Core colors")},
|
||||
{QStringLiteral("section"),
|
||||
QCoreApplication::translate("CableTypeList", "Cable section")},
|
||||
{QStringLiteral("color"),
|
||||
QCoreApplication::translate("CableTypeList", "Sheath color")}
|
||||
};
|
||||
|
||||
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
|
||||
//("Article number" 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);
|
||||
}
|
||||
}
|
||||
|
||||
//The wording older files were written with : these French names
|
||||
//were the ones the label line carried before the interface text
|
||||
//became English, and a catalogue written with them must keep
|
||||
//reading, so each of them still means its column here.
|
||||
static const QMap<QString, QString> older_wording = {
|
||||
{QStringLiteral("désignation"), QStringLiteral("designation")},
|
||||
{QStringLiteral("nombre d'âmes"), QStringLiteral("cores")},
|
||||
{QStringLiteral("couleurs des âmes"), QStringLiteral("core_colors")},
|
||||
{QStringLiteral("section du câble"), QStringLiteral("section")},
|
||||
{QStringLiteral("couleur du fourreau"), QStringLiteral("color")}
|
||||
};
|
||||
for (auto it = older_wording.constBegin(); it != older_wording.constEnd(); ++it)
|
||||
{
|
||||
if (!map.contains(it.key())) {
|
||||
map.insert(it.key(), it.value());
|
||||
}
|
||||
}
|
||||
|
||||
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", "No receiver for the cable types file.");
|
||||
}
|
||||
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);
|
||||
|
||||
//The folder may not be there yet: a fresh profile has no
|
||||
//documents folder, and a path typed into the settings may name a
|
||||
//folder which was never created. QSaveFile cannot write into a
|
||||
//folder which does not exist, so without this the whole write
|
||||
//fails and the message says nothing about the real reason.
|
||||
const QString folder = QFileInfo(path).absolutePath();
|
||||
if (!folder.isEmpty() && !QDir(folder).exists() && !QDir().mkpath(folder))
|
||||
{
|
||||
if (error) {
|
||||
*error = QCoreApplication::translate(
|
||||
"CableTypeList",
|
||||
"Cannot create the folder %1 for the cable types file.").arg(folder);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
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", "The file contains no header: missing columns.");
|
||||
}
|
||||
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", "The file contains no header: missing columns.");
|
||||
}
|
||||
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",
|
||||
"This entry no longer exists in the file: it may have been modified in the meantime.");
|
||||
}
|
||||
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", "The file already exists and is not empty.");
|
||||
}
|
||||
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