/* 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 . */ #include "edzpart.h" #include #include #include #include #include namespace { /** Parse EPLAN's `de_DE@..;en_EN@..;..` multilingual blob into ordered (full_code, text) pairs. Values may contain `;;` as an in-text separator, so we split on language *markers* (xx_YY@) rather than on bare `;`. */ QList> langDict(const QString &value) { QList> out; if (value.isEmpty()) { return out; } static const QRegularExpression re(QStringLiteral("[a-z]{2}_[A-Z]{2}@")); QList marks; auto it = re.globalMatch(value); while (it.hasNext()) { marks.append(it.next()); } for (int i = 0; i < marks.size(); ++i) { const QRegularExpressionMatch &m = marks.at(i); const QString code = value.mid(m.capturedStart(), m.capturedLength() - 1); // drop '@' const int text_start = m.capturedEnd(); const int text_end = (i + 1 < marks.size()) ? marks.at(i + 1).capturedStart() : value.size(); QString text = value.mid(text_start, text_end - text_start); while (!text.isEmpty() && text.back().isSpace()) { text.chop(1); } while (!text.isEmpty() && text.back() == QLatin1Char(';')) { text.chop(1); } text.replace(QStringLiteral(";;"), QStringLiteral("; ")); text = text.trimmed(); if (!text.isEmpty()) { out.append({code, text}); } } return out; } /** Pick one language: preferred English variants, then any English, then first. */ QString pickLang(const QString &value) { if (value.isEmpty()) { return QString(); } const QList> d = langDict(value); if (d.isEmpty()) { return value.trimmed(); } for (const QString &code : {QStringLiteral("en_EN"), QStringLiteral("en_US"), QStringLiteral("en_GB")}) { for (const auto &p : d) { if (p.first == code && !p.second.isEmpty()) { return p.second; } } } for (const auto &p : d) { if (p.first.startsWith(QStringLiteral("en_")) && !p.second.isEmpty()) { return p.second; } } return d.first().second; } /** {2-letter QET lang -> text}; first variant of each language wins. */ QMap multilangNames(const QString &value) { QMap names; for (const auto &p : langDict(value)) { const QString s = p.first.left(2).toLower(); if (!names.contains(s)) { names.insert(s, p.second); } } return names; } /** Tokenise a designation into (is-text, number, text) runs for natural sort. */ struct NatTok { int type; qlonglong num; QString str; }; QList natKey(const QString &s) { QList out; static const QRegularExpression re(QStringLiteral("\\d+|\\D+")); auto it = re.globalMatch(s); while (it.hasNext()) { const QString t = it.next().captured(); bool all_digit = !t.isEmpty(); for (const QChar c : t) { if (!c.isDigit()) { all_digit = false; break; } } if (all_digit) { out.append({0, t.toLongLong(), QString()}); } else { out.append({1, 0, t.toLower()}); } } return out; } bool natLess(const QString &a, const QString &b) { const QList ka = natKey(a), kb = natKey(b); const int n = qMin(ka.size(), kb.size()); for (int i = 0; i < n; ++i) { if (ka[i].type != kb[i].type) { return ka[i].type < kb[i].type; } if (ka[i].type == 0) { if (ka[i].num != kb[i].num) return ka[i].num < kb[i].num; } else { if (ka[i].str != kb[i].str) return ka[i].str < kb[i].str; } } return ka.size() < kb.size(); } } // namespace /** Parse the part.xml at @a part_xml_path into this object. */ bool EdzPart::parse(const QString &part_xml_path) { m_error.clear(); QFile file(part_xml_path); if (!file.open(QIODevice::ReadOnly)) { m_error = QStringLiteral("Cannot open %1").arg(part_xml_path); return false; } QDomDocument doc; QString parse_err; int line = 0, col = 0; if (!doc.setContent(&file, &parse_err, &line, &col)) { m_error = QStringLiteral("Malformed part.xml (line %1): %2") .arg(line).arg(parse_err); return false; } file.close(); const QDomElement part = doc.documentElement().firstChildElement(QStringLiteral("part")); if (part.isNull()) { m_error = QStringLiteral("No element in part.xml"); return false; } m_part_number = part.attribute(QStringLiteral("P_ARTICLE_PARTNR")); m_manufacturer = part.attribute(QStringLiteral("P_ARTICLE_MANUFACTURER")); m_order_number = part.attribute(QStringLiteral("P_ARTICLE_ORDERNR")); m_type_number = part.attribute(QStringLiteral("P_ARTICLE_TYPENR")); const QString descr1 = part.attribute(QStringLiteral("P_ARTICLE_DESCR1")); const QString descr2 = part.attribute(QStringLiteral("P_ARTICLE_DESCR2")); m_description = pickLang(descr1); m_comment = pickLang(descr2); m_names = multilangNames(descr1); const QString pic = part.attribute(QStringLiteral("P_ARTICLE_PICTUREFILE")); if (!pic.isEmpty()) { m_picture = QFileInfo(QString(pic).replace('\\', '/')).fileName(); } // Pins = function templates that carry a physical connection designation. const QDomNodeList fts = part.elementsByTagName(QStringLiteral("functiontemplate")); for (int i = 0; i < fts.size(); ++i) { const QDomElement ft = fts.at(i).toElement(); const QString desig = ft.attribute(QStringLiteral("connectionDesignation")).trimmed(); if (desig.isEmpty()) { continue; } EdzPin pin; pin.designation = desig; pin.description = ft.attribute(QStringLiteral("connectiondescription")).trimmed(); m_pins.append(pin); } // EPLAN lists pins in its own order (e.g. MFH200: 1,3,4,2); order them by // pin number so they stack 1,2,3,4 on the symbol. std::stable_sort(m_pins.begin(), m_pins.end(), [](const EdzPin &a, const EdzPin &b) { return natLess(a.designation, b.designation); }); return true; }