/* This file is part of the KDE libraries SPDX-FileCopyrightText: 2000 Simon Hausmann SPDX-FileCopyrightText: 2000 Kurt Granroth SPDX-License-Identifier: LGPL-2.0-only */ #include "kxmlguiclient.h" #include "debug.h" #include "kactioncollection.h" #include "kxmlguibuilder.h" #include "kxmlguifactory.h" #include "kxmlguiversionhandler_p.h" #include #include #include #include #include #include #include #include #include #include class KXMLGUIClientPrivate { public: KXMLGUIClientPrivate() : m_componentName(QCoreApplication::applicationName()) , m_textTagNames({QStringLiteral("text"), QStringLiteral("Text"), QStringLiteral("title")}) { } ~KXMLGUIClientPrivate() { } bool mergeXML(QDomElement &base, QDomElement &additive, KActionCollection *actionCollection); bool isEmptyContainer(const QDomElement &base, KActionCollection *actionCollection) const; QDomElement findMatchingElement(const QDomElement &base, const QDomElement &additive); QString m_componentName; QDomDocument m_doc; KActionCollection *m_actionCollection = nullptr; QDomDocument m_buildDocument; QPointer m_factory; KXMLGUIClient *m_parent = nullptr; // QPtrList m_supers; QList m_children; KXMLGUIBuilder *m_builder = nullptr; QString m_xmlFile; QString m_localXMLFile; const QStringList m_textTagNames; // Actions to enable/disable on a state change QMap m_actionsStateMap; }; KXMLGUIClient::KXMLGUIClient() : d(new KXMLGUIClientPrivate) { } KXMLGUIClient::KXMLGUIClient(KXMLGUIClient *parent) : d(new KXMLGUIClientPrivate) { Q_INIT_RESOURCE(kxmlgui); parent->insertChildClient(this); } KXMLGUIClient::~KXMLGUIClient() { if (d->m_parent) { d->m_parent->removeChildClient(this); } if (d->m_factory) { qCWarning(DEBUG_KXMLGUI) << this << "deleted without having been removed from the factory first. This will leak standalone popupmenus and could lead to crashes."; d->m_factory->forgetClient(this); } for (KXMLGUIClient *client : std::as_const(d->m_children)) { if (d->m_factory) { d->m_factory->forgetClient(client); } assert(client->d->m_parent == this); client->d->m_parent = nullptr; } delete d->m_actionCollection; } QAction *KXMLGUIClient::action(const QString &name) const { QAction *act = actionCollection()->action(name); if (!act) { for (KXMLGUIClient *client : std::as_const(d->m_children)) { act = client->actionCollection()->action(name); if (act) { break; } } } return act; } KActionCollection *KXMLGUIClient::actionCollection() const { if (!d->m_actionCollection) { d->m_actionCollection = new KActionCollection(this); d->m_actionCollection->setObjectName(QStringLiteral("KXMLGUIClient-KActionCollection")); } return d->m_actionCollection; } QAction *KXMLGUIClient::action(const QDomElement &element) const { return actionCollection()->action(element.attribute(QStringLiteral("name"))); } QString KXMLGUIClient::componentName() const { return d->m_componentName; } QDomDocument KXMLGUIClient::domDocument() const { return d->m_doc; } QString KXMLGUIClient::xmlFile() const { return d->m_xmlFile; } QString KXMLGUIClient::localXMLFile() const { if (!d->m_localXMLFile.isEmpty()) { return d->m_localXMLFile; } if (!QDir::isRelativePath(d->m_xmlFile)) { return QString(); // can't save anything here } if (d->m_xmlFile.isEmpty()) { // setXMLFile not called at all, can't save. Use case: ToolBarHandler return QString(); } return QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/kxmlgui5/%1/%2").arg(componentName(), d->m_xmlFile); } void KXMLGUIClient::reloadXML() { // TODO: this method can't be used for the KXmlGuiWindow, since it doesn't merge in ui_standards.rc! // -> KDE5: load ui_standards_rc in setXMLFile using a flag, and remember that flag? // and then KEditToolBar can use reloadXML. QString file(xmlFile()); if (!file.isEmpty()) { setXMLFile(file); } } void KXMLGUIClient::setComponentName(const QString &componentName, const QString &componentDisplayName) { d->m_componentName = componentName; actionCollection()->setComponentName(componentName); actionCollection()->setComponentDisplayName(componentDisplayName); if (d->m_builder) { d->m_builder->setBuilderClient(this); } } QString KXMLGUIClient::standardsXmlFileLocation() { if (QStandardPaths::isTestModeEnabled()) { return QStringLiteral(":/kxmlgui5/ui_standards.rc"); } QString file = QStandardPaths::locate(QStandardPaths::GenericConfigLocation, QStringLiteral("kxmlgui5/ui_standards.rc")); if (file.isEmpty()) { // fallback to resource, to allow to use the rc file compiled into this framework, must exist! file = QStringLiteral(":/kxmlgui5/ui_standards.rc"); Q_ASSERT(QFile::exists(file)); } return file; } void KXMLGUIClient::loadStandardsXmlFile() { setXML(KXMLGUIFactory::readConfigFile(standardsXmlFileLocation())); } void KXMLGUIClient::setXMLFile(const QString &_file, bool merge, bool setXMLDoc) { // store our xml file name if (!_file.isNull()) { d->m_xmlFile = _file; } if (!setXMLDoc) { return; } QString file = _file; QStringList allFiles; if (!QDir::isRelativePath(file)) { allFiles.append(file); } else { const QString filter = componentName() + QLatin1Char('/') + _file; // files on filesystem allFiles << QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("kxmlgui5/") + filter); // built-in resource file const QString qrcFile(QLatin1String(":/kxmlgui5/") + filter); if (QFile::exists(qrcFile)) { allFiles << qrcFile; } } if (allFiles.isEmpty() && !_file.isEmpty()) { // if a non-empty file gets passed and we can't find it, // inform the developer using some debug output qCWarning(DEBUG_KXMLGUI) << "cannot find .rc file" << _file << "for component" << componentName(); } // make sure to merge the settings from any file specified by setLocalXMLFile() if (!d->m_localXMLFile.isEmpty() && !file.endsWith(QLatin1String("ui_standards.rc"))) { const bool exists = QDir::isRelativePath(d->m_localXMLFile) || QFile::exists(d->m_localXMLFile); if (exists && !allFiles.contains(d->m_localXMLFile)) { allFiles.prepend(d->m_localXMLFile); } } QString doc; if (!allFiles.isEmpty()) { file = findMostRecentXMLFile(allFiles, doc); } // Always call setXML, even on error, so that we don't keep all ui_standards.rc menus. setXML(doc, merge); } void KXMLGUIClient::setLocalXMLFile(const QString &file) { d->m_localXMLFile = file; } void KXMLGUIClient::replaceXMLFile(const QString &xmlfile, const QString &localxmlfile, bool merge) { if (!QDir::isAbsolutePath(xmlfile)) { qCWarning(DEBUG_KXMLGUI) << "xml file" << xmlfile << "is not an absolute path"; } setLocalXMLFile(localxmlfile); setXMLFile(xmlfile, merge); } // The top document element may have translation domain attribute set, // or the translation domain may be implicitly the application domain. // This domain must be used to fetch translations for all text elements // in the document that do not have their own domain attribute. // In order to preserve this semantics through document mergings, // the top or application domain must be propagated to all text elements // lacking their own domain attribute. static void propagateTranslationDomain(QDomDocument &doc, const QStringList &tagNames) { const QLatin1String attrDomain("translationDomain"); QDomElement base = doc.documentElement(); QString domain = base.attribute(attrDomain); if (domain.isEmpty()) { domain = QString::fromUtf8(KLocalizedString::applicationDomain()); if (domain.isEmpty()) { return; } } for (const QString &tagName : tagNames) { QDomNodeList textNodes = base.elementsByTagName(tagName); for (int i = 0; i < textNodes.length(); ++i) { QDomElement e = textNodes.item(i).toElement(); QString localDomain = e.attribute(attrDomain); if (localDomain.isEmpty()) { e.setAttribute(attrDomain, domain); } } } } void KXMLGUIClient::setXML(const QString &document, bool merge) { QDomDocument doc; // QDomDocument raises a parse error on empty document, but we accept no app-specific document, // in which case you only get ui_standards.rc layout. if (!document.isEmpty()) { const QDomDocument::ParseResult result = doc.setContent(document); if (!result) { qCCritical(DEBUG_KXMLGUI) << "Error parsing XML document:" << result.errorMessage << "at line" << result.errorLine << "column" << result.errorColumn; #ifdef NDEBUG setDOMDocument(QDomDocument(), merge); // otherwise empty menus from ui_standards.rc stay around #else abort(); #endif return; } } propagateTranslationDomain(doc, d->m_textTagNames); setDOMDocument(doc, merge); } void KXMLGUIClient::setDOMDocument(const QDomDocument &document, bool merge) { if (merge && !d->m_doc.isNull()) { QDomElement base = d->m_doc.documentElement(); QDomElement e = document.documentElement(); // merge our original (global) xml with our new one d->mergeXML(base, e, actionCollection()); // reassign our pointer as mergeXML might have done something // strange to it base = d->m_doc.documentElement(); // qCDebug(DEBUG_KXMLGUI) << "Result of xmlgui merging:" << d->m_doc.toString(); // we want some sort of failsafe.. just in case if (base.isNull()) { d->m_doc = document; } } else { d->m_doc = document; } setXMLGUIBuildDocument(QDomDocument()); } // if (equals(a,b)) is more readable than if (a.compare(b, Qt::CaseInsensitive)==0) static inline bool equalstr(const QString &a, const QString &b) { return a.compare(b, Qt::CaseInsensitive) == 0; } static inline bool equalstr(const QString &a, QLatin1String b) { return a.compare(b, Qt::CaseInsensitive) == 0; } bool KXMLGUIClientPrivate::mergeXML(QDomElement &base, QDomElement &additive, KActionCollection *actionCollection) { const QLatin1String tagAction("Action"); const QLatin1String tagMerge("Merge"); const QLatin1String tagSeparator("Separator"); const QLatin1String tagMergeLocal("MergeLocal"); const QLatin1String tagText("text"); const QLatin1String attrAppend("append"); const QString attrName(QStringLiteral("name")); const QString attrWeakSeparator(QStringLiteral("weakSeparator")); const QString attrAlreadyVisited(QStringLiteral("alreadyVisited")); const QString attrNoMerge(QStringLiteral("noMerge")); const QLatin1String attrOne("1"); // there is a possibility that we don't want to merge in the // additive.. rather, we might want to *replace* the base with the // additive. this can be for any container.. either at a file wide // level or a simple container level. we look for the 'noMerge' // tag, in any event and just replace the old with the new if (additive.attribute(attrNoMerge) == attrOne) { // ### use toInt() instead? (Simon) base.parentNode().replaceChild(additive, base); return true; } else { // Merge attributes { const QDomNamedNodeMap attribs = additive.attributes(); const int attribcount = attribs.count(); for (int i = 0; i < attribcount; ++i) { const QDomNode node = attribs.item(i); base.setAttribute(node.nodeName(), node.nodeValue()); } } // iterate over all elements in the container (of the global DOM tree) QDomNode n = base.firstChild(); while (!n.isNull()) { QDomElement e = n.toElement(); n = n.nextSibling(); // Advance now so that we can safely delete e if (e.isNull()) { continue; } const QString tag = e.tagName(); // if there's an action tag in the global tree and the action is // not implemented, then we remove the element if (equalstr(tag, tagAction)) { const QString name = e.attribute(attrName); if (!actionCollection->action(name) || !KAuthorized::authorizeAction(name)) { // remove this child as we aren't using it base.removeChild(e); continue; } } // if there's a separator defined in the global tree, then add an // attribute, specifying that this is a "weak" separator else if (equalstr(tag, tagSeparator)) { e.setAttribute(attrWeakSeparator, uint(1)); // okay, hack time. if the last item was a weak separator OR // this is the first item in a container, then we nuke the // current one QDomElement prev = e.previousSibling().toElement(); if (prev.isNull() // || (equalstr(prev.tagName(), tagSeparator) && !prev.attribute(attrWeakSeparator).isNull()) // || (equalstr(prev.tagName(), tagText))) { // the previous element was a weak separator or didn't exist base.removeChild(e); continue; } } // the MergeLocal tag lets us specify where non-standard elements // of the local tree shall be merged in. After inserting the // elements we delete this element else if (equalstr(tag, tagMergeLocal)) { QDomNode it = additive.firstChild(); while (!it.isNull()) { QDomElement newChild = it.toElement(); it = it.nextSibling(); if (newChild.isNull()) { continue; } if (equalstr(newChild.tagName(), tagText)) { continue; } if (newChild.attribute(attrAlreadyVisited) == attrOne) { continue; } QString itAppend(newChild.attribute(attrAppend)); QString elemName(e.attribute(attrName)); if ((itAppend.isNull() && elemName.isEmpty()) || (itAppend == elemName)) { // first, see if this new element matches a standard one in // the global file. if it does, then we skip it as it will // be merged in, later QDomElement matchingElement = findMatchingElement(newChild, base); if (matchingElement.isNull() || equalstr(newChild.tagName(), tagSeparator)) { base.insertBefore(newChild, e); } } } base.removeChild(e); continue; } else if (equalstr(tag, tagText)) { continue; } else if (equalstr(tag, tagMerge)) { continue; } // in this last case we check for a separator tag and, if not, we // can be sure that it is a container --> proceed with child nodes // recursively and delete the just proceeded container item in // case it is empty (if the recursive call returns true) else { QDomElement matchingElement = findMatchingElement(e, additive); if (!matchingElement.isNull()) { matchingElement.setAttribute(attrAlreadyVisited, uint(1)); if (mergeXML(e, matchingElement, actionCollection)) { base.removeChild(e); additive.removeChild(matchingElement); // make sure we don't append it below continue; } continue; } else { // this is an important case here! We reach this point if the // "local" tree does not contain a container definition for // this container. However we have to call mergeXML recursively // and make it check if there are actions implemented for this // container. *If* none, then we can remove this container now QDomElement dummy; if (mergeXML(e, dummy, actionCollection)) { base.removeChild(e); } continue; } } } // here we append all child elements which were not inserted // previously via the LocalMerge tag n = additive.firstChild(); while (!n.isNull()) { QDomElement e = n.toElement(); n = n.nextSibling(); // Advance now so that we can safely delete e if (e.isNull()) { continue; } QDomElement matchingElement = findMatchingElement(e, base); if (matchingElement.isNull()) { base.appendChild(e); } } // do one quick check to make sure that the last element was not // a weak separator QDomElement last = base.lastChild().toElement(); if (equalstr(last.tagName(), tagSeparator) && (!last.attribute(attrWeakSeparator).isNull())) { base.removeChild(last); } } return isEmptyContainer(base, actionCollection); } bool KXMLGUIClientPrivate::isEmptyContainer(const QDomElement &base, KActionCollection *actionCollection) const { // now we check if we are empty (in which case we return "true", to // indicate the caller that it can delete "us" (the base element // argument of "this" call) QDomNode n = base.firstChild(); while (!n.isNull()) { const QDomElement e = n.toElement(); n = n.nextSibling(); // Advance now so that we can safely delete e if (e.isNull()) { continue; } const QString tag = e.tagName(); if (equalstr(tag, QLatin1String("Action"))) { // if base contains an implemented action, then we must not get // deleted (note that the actionCollection contains both, // "global" and "local" actions) if (actionCollection->action(e.attribute(QStringLiteral("name")))) { return false; } } else if (equalstr(tag, QLatin1String("Separator"))) { // if we have a separator which has *not* the weak attribute // set, then it must be owned by the "local" tree in which case // we must not get deleted either const QString weakAttr = e.attribute(QStringLiteral("weakSeparator")); if (weakAttr.isEmpty() || weakAttr.toInt() != 1) { return false; } } else if (equalstr(tag, QLatin1String("merge"))) { continue; } // a text tag is NOT enough to spare this container else if (equalstr(tag, QLatin1String("text"))) { continue; } // what's left are non-empty containers! *don't* delete us in this // case (at this position we can be *sure* that the container is // *not* empty, as the recursive call for it was in the first loop // which deleted the element in case the call returned "true" else { return false; } } return true; // I'm empty, please delete me. } QDomElement KXMLGUIClientPrivate::findMatchingElement(const QDomElement &base, const QDomElement &additive) { const QString idAttribute(base.tagName() == QLatin1String("ActionProperties") ? QStringLiteral("scheme") : QStringLiteral("name")); QDomNode n = additive.firstChild(); while (!n.isNull()) { QDomElement e = n.toElement(); n = n.nextSibling(); // Advance now so that we can safely delete e -- TODO we don't, so simplify this if (e.isNull()) { continue; } const QString tag = e.tagName(); // skip all action and merge tags as we will never use them if (equalstr(tag, QLatin1String("Action")) // || equalstr(tag, QLatin1String("MergeLocal"))) { continue; } // now see if our tags are equivalent if (equalstr(tag, base.tagName()) // && e.attribute(idAttribute) == base.attribute(idAttribute)) { return e; } } // nope, return a (now) null element return QDomElement(); } void KXMLGUIClient::setXMLGUIBuildDocument(const QDomDocument &doc) { d->m_buildDocument = doc; } QDomDocument KXMLGUIClient::xmlguiBuildDocument() const { return d->m_buildDocument; } void KXMLGUIClient::setFactory(KXMLGUIFactory *factory) { d->m_factory = factory; } KXMLGUIFactory *KXMLGUIClient::factory() const { return d->m_factory; } KXMLGUIClient *KXMLGUIClient::parentClient() const { return d->m_parent; } void KXMLGUIClient::insertChildClient(KXMLGUIClient *child) { if (child->d->m_parent) { child->d->m_parent->removeChildClient(child); } d->m_children.append(child); child->d->m_parent = this; } void KXMLGUIClient::removeChildClient(KXMLGUIClient *child) { assert(d->m_children.contains(child)); d->m_children.removeAll(child); child->d->m_parent = nullptr; } /*bool KXMLGUIClient::addSuperClient( KXMLGUIClient *super ) { if ( d->m_supers.contains( super ) ) return false; d->m_supers.append( super ); return true; }*/ QList KXMLGUIClient::childClients() { return d->m_children; } void KXMLGUIClient::setClientBuilder(KXMLGUIBuilder *builder) { d->m_builder = builder; } KXMLGUIBuilder *KXMLGUIClient::clientBuilder() const { return d->m_builder; } void KXMLGUIClient::plugActionList(const QString &name, const QList &actionList) { if (!d->m_factory) { return; } d->m_factory->plugActionList(this, name, actionList); } void KXMLGUIClient::unplugActionList(const QString &name) { if (!d->m_factory) { return; } d->m_factory->unplugActionList(this, name); } QString KXMLGUIClient::findMostRecentXMLFile(const QStringList &files, QString &doc) { KXmlGuiVersionHandler versionHandler(files); doc = versionHandler.finalDocument(); return versionHandler.finalFile(); } void KXMLGUIClient::addStateActionEnabled(const QString &state, const QString &action) { StateChange stateChange = getActionsToChangeForState(state); stateChange.actionsToEnable.append(action); // qCDebug(DEBUG_KXMLGUI) << "KXMLGUIClient::addStateActionEnabled( " << state << ", " << action << ")"; d->m_actionsStateMap.insert(state, stateChange); } void KXMLGUIClient::addStateActionDisabled(const QString &state, const QString &action) { StateChange stateChange = getActionsToChangeForState(state); stateChange.actionsToDisable.append(action); // qCDebug(DEBUG_KXMLGUI) << "KXMLGUIClient::addStateActionDisabled( " << state << ", " << action << ")"; d->m_actionsStateMap.insert(state, stateChange); } KXMLGUIClient::StateChange KXMLGUIClient::getActionsToChangeForState(const QString &state) { return d->m_actionsStateMap[state]; } void KXMLGUIClient::stateChanged(const QString &newstate, KXMLGUIClient::ReverseStateChange reverse) { const StateChange stateChange = getActionsToChangeForState(newstate); bool setTrue = (reverse == StateNoReverse); bool setFalse = !setTrue; // Enable actions which need to be enabled... // for (const auto &actionId : stateChange.actionsToEnable) { QAction *action = actionCollection()->action(actionId); if (action) { action->setEnabled(setTrue); } } // and disable actions which need to be disabled... // for (const auto &actionId : stateChange.actionsToDisable) { QAction *action = actionCollection()->action(actionId); if (action) { action->setEnabled(setFalse); } } } void KXMLGUIClient::beginXMLPlug(QWidget *w) { actionCollection()->addAssociatedWidget(w); for (KXMLGUIClient *client : std::as_const(d->m_children)) { client->beginXMLPlug(w); } } void KXMLGUIClient::endXMLPlug() { } void KXMLGUIClient::prepareXMLUnplug(QWidget *w) { actionCollection()->removeAssociatedWidget(w); for (KXMLGUIClient *client : std::as_const(d->m_children)) { client->prepareXMLUnplug(w); } } void KXMLGUIClient::virtual_hook(int, void *) { /*BASE::virtual_hook( id, data );*/ } QString KXMLGUIClient::findVersionNumber(const QString &xml) { enum { ST_START, ST_AFTER_OPEN, ST_AFTER_GUI, ST_EXPECT_VERSION, ST_VERSION_NUM, } state = ST_START; const int length = xml.length(); for (int pos = 0; pos < length; pos++) { switch (state) { case ST_START: if (xml[pos] == QLatin1Char('<')) { state = ST_AFTER_OPEN; } break; case ST_AFTER_OPEN: { // Jump to gui.. const int guipos = xml.indexOf(QLatin1String("gui"), pos, Qt::CaseInsensitive); if (guipos == -1) { return QString(); // Reject } pos = guipos + 2; // Position at i, so we're moved ahead to the next character by the ++; state = ST_AFTER_GUI; break; } case ST_AFTER_GUI: state = ST_EXPECT_VERSION; break; case ST_EXPECT_VERSION: { const int verpos = xml.indexOf(QLatin1String("version"), pos, Qt::CaseInsensitive); if (verpos == -1) { return QString(); // Reject } pos = verpos + 7; // strlen("version") is 7 while (xml.at(pos).isSpace()) { ++pos; } if (xml.at(pos++) != QLatin1Char('=')) { return QString(); // Reject } while (xml.at(pos).isSpace()) { ++pos; } state = ST_VERSION_NUM; break; } case ST_VERSION_NUM: { int endpos; for (endpos = pos; endpos < length; endpos++) { const ushort ch = xml[endpos].unicode(); if (ch >= QLatin1Char('0') && ch <= QLatin1Char('9')) { continue; // Number.. } if (ch == QLatin1Char('"')) { // End of parameter break; } else { // This shouldn't be here.. endpos = length; } } if (endpos != pos && endpos < length) { const QString matchCandidate = xml.mid(pos, endpos - pos); // Don't include " ". return matchCandidate; } state = ST_EXPECT_VERSION; // Try to match a well-formed version.. break; } // case.. } // switch } // for return QString(); }