diff --git a/sources/autoNum/assignvariables.cpp b/sources/autoNum/assignvariables.cpp index 8e2d465ec..c8059bbf2 100644 --- a/sources/autoNum/assignvariables.cpp +++ b/sources/autoNum/assignvariables.cpp @@ -24,6 +24,8 @@ #include "../qetgraphicsitem/element.h" #include "../qetxml.h" #include "../qetproject.h" +#include +#include #include #include #include @@ -703,6 +705,74 @@ namespace autonum return formula; } + /** + @brief prefixFromLabelFile + Look up a prefix for @a path (path[dirLevel] outermost, path[1] the + deepest directory; path[0], the element's own file name, is never + matched) in the qet_labels.xml at @a filepath. + + Descends through nested \ elements matching + path[dirLevel], path[dirLevel-1], ..., path[1] in turn, considering + only *direct* children at each step -- unlike a flat token scan, + this cannot be fooled by a same-named category living elsewhere in + the document at the wrong nesting depth (bugtracker #671 item 5). + + At each matched level, that category's own \ child -- even + an empty one -- overrides whatever a shallower ancestor already + provided, so an explicit empty \ cancels inheritance + rather than silently falling back to it (the behaviour requested in + PR #686 review). A category with no \ child at all leaves + the inherited value untouched, which is how a directory with no + prefix of its own comes to inherit its parent's, as the file's own + header comment documents. + + @return the prefix that applies, or a null QString if the file + cannot be read, is not well-formed, or does not describe this + path at all (as opposed to describing it with no prefix + anywhere along it, which is a non-null empty string). + */ + static QString prefixFromLabelFile(const QString &filepath, const QStringList &path, int dirLevel) + { + QFile file(filepath); + if (!file.open(QFile::ReadOnly | QFile::Text)) + return QString(); + + QDomDocument document; + if (!document.setContent(&file)) + return QString(); + + QDomElement node = document.documentElement(); + if (node.isNull()) + return QString(); + + QString prefix; + for (int i = dirLevel ; i >= 1 ; --i) { + QDomElement child = node.firstChildElement(QStringLiteral("category")); + while (!child.isNull() + && child.attribute(QStringLiteral("name")) != path[i]) { + child = child.nextSiblingElement(QStringLiteral("category")); + } + if (child.isNull()) + return QString(); + node = child; + + const QDomElement own = node.firstChildElement(QStringLiteral("prefix")); + if (!own.isNull()) { + //readElementText()'s null-vs-empty distinction that PR + //#686 needed for the old QXmlStreamReader-based lookup + //has a QDomElement equivalent: text() on an empty + //element can itself come back null depending on how the + //XML was written, so the same explicit fallback applies + //-- an empty QString here means "found, deliberately + //blank", not "not found". + prefix = own.text(); + if (prefix.isNull()) + prefix = QString(""); + } + } + return prefix; + } + /** @brief elementPrefixForLocation @param location @@ -716,114 +786,85 @@ namespace autonum if (!location.isProject()) return QString(); - QXmlStreamReader rxml; - QString path[10]; - int i = -1; + //Directory names from the element up to (not including) the + //collection root, outermost last -- path[dirLevel] is the + //top-level category, path[1] the element's immediate parent + //directory, path[0] the element's own file name (never matched + //against a category: the search stops descending once it has + //matched path[1], the deepest real directory). An unbounded + //QStringList rather than a fixed-size array, because a custom + //collection can nest deeper than the shipped one -- see + //bugtracker #671 item 3. + QStringList path; ElementsLocation current_location = location; - int dirLevel = -1; - - //Add location name to path array - while((current_location.parent() != current_location) && (current_location.parent().fileName() != "import")) + while ((current_location.parent() != current_location) + && (current_location.parent().fileName() != "import")) { - i++; - path[i]=current_location.fileName(); + path << current_location.fileName(); current_location = current_location.parent(); - dirLevel++; } - //User Element without folder treatment - if (i == -1) - { - i = 0; - path[i]=current_location.fileName(); + //User element without folder treatment + if (path.isEmpty()) { + path << current_location.fileName(); current_location = current_location.parent(); - dirLevel = 0; + } + const int dirLevel = path.size() - 1; + //Name of the top-level tree the element's path was found + //under, e.g. "10_electric" -- or, for a custom/company + //collection not organised that way, whatever its top-level + //folder happens to be called. + const QString collection_root = current_location.fileName(); + + //Every top-level common-collection tree (10_electric, + //20_logic, 30_hydraulic, ...) may carry its own + //qet_labels.xml, with categories relative to that tree, the + //same way 10_electric/qet_labels.xml already does -- not just + //10_electric, which is all the hardcoded check this replaces + //used to allow (bugtracker #671 item 2). commonElementsDir() + //-- unlike customElementsDir(), which normalises this itself + //-- returns whatever path the user configured verbatim, with + //no guaranteed trailing separator; concatenating a suffix onto + //it directly used to silently mangle the path (and so the + //prefix lookup) for any install relocated to a directory + //without a trailing slash (#671 item 1). QDir::filePath() + //joins correctly either way. + { + const QString common_file = QDir(QETApp::commonElementsDir()) + .filePath(collection_root + QStringLiteral("/qet_labels.xml")); + const QString prefix = prefixFromLabelFile(common_file, path, dirLevel); + if (!prefix.isNull()) { + return prefix; + } } - // Create Custom labels if qet_labels.xml exits in customElementsDir - if (current_location.fileName() != "10_electric"){ - QString custom_labels = "qet_labels.xml"; - QString customfilepath = QETApp::customElementsDir().append(custom_labels); - - QFile file(customfilepath); - file.isReadable(); - if (!file.open(QFile::ReadOnly | QFile::Text)) - return QString(); - rxml.setDevice(&file); - rxml.readNext(); + /* Which collection an element actually came from is not + * recoverable post-import (addElement() strips the protocol), + * so custom and company labels files are tried against two + * possible layouts: with the collection's top-level tree name + * folded into the path (a custom/company file organised as a + * mirror of the common collection, tree name included) and + * without it (a file scoped to just this one tree, matching + * how the common collection's own files are written). Custom + * is tried before company, so a user override wins over a + * shared one. + */ + QStringList path_from_root = path; + path_from_root << collection_root; - while(!rxml.atEnd()) - { - if (rxml.attributes().value("name").toString() == path[i]) - { - rxml.readNext(); - i=i-1; - //reached element directory - if (i==0) - { - for (int j=i; j<= dirLevel; j = j +1) - { - //if there is a prefix available apply prefix - if(rxml.name().toString()=="prefix") - { - return rxml.readElementText(); - } - //if there isn't a prefix available, find parent prefix in parent folder - else - { - while (rxml.readNextStartElement() && rxml.name().toString()!="prefix") - { - rxml.skipCurrentElement(); - rxml.readNext(); - } - } - } + const QStringList candidate_dirs = { + QETApp::customElementsDir(), + QETApp::companyElementsDir() + }; + for (const QString &dir : candidate_dirs) { + const QString candidate = + QDir(dir).filePath(QStringLiteral("qet_labels.xml")); + for (const QStringList &segments : {path_from_root, path}) { + const QString prefix = prefixFromLabelFile( + candidate, segments, segments.size() - 1); + if (!prefix.isNull()) { + return prefix; } } - rxml.readNext(); - } - } - else - { - QString qet_labels = "10_electric/qet_labels.xml"; - QString filepath = QETApp::commonElementsDir().append(qet_labels); - QFile file(filepath); - file.isReadable(); - if (!file.open(QFile::ReadOnly | QFile::Text)) - return QString(); - - rxml.setDevice(&file); - rxml.readNext(); - - while(!rxml.atEnd()) - { - if (rxml.attributes().value("name").toString() == path[i]) - { - rxml.readNext(); - i=i-1; - //reached element directory - if (i==0) - { - for (int j=i; j<= dirLevel; j = j +1) - { - //if there is a prefix available apply prefix - if(rxml.name().toString()=="prefix") - { - return rxml.readElementText(); - } - //if there isn't a prefix available, find parent prefix in parent folder - else - { - while (rxml.readNextStartElement() && rxml.name().toString()!="prefix") - { - rxml.skipCurrentElement(); - rxml.readNext(); - } - } - } - } - } - rxml.readNext(); - } } return QString(); }