/* 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 . */ #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 #include #include #include /** @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>(); 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 cables = m_project->cables(); for (Cable *cable : cables) { m_watchers << connect(cable, &Cable::changed, this, &CableListModel::refresh); } const QList diagrams = m_project->diagrams(); for (Diagram *diagram : diagrams) { m_watchers << connect(diagram, &Diagram::diagramInformationChanged, this, &CableListModel::refresh); const QList 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>(); 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())); auto me = QMetaEnum::fromType(); 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> 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>())); 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(); 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); }