1 | /* |
2 | This file is part of the KDE libraries |
3 | SPDX-FileCopyrightText: 2000 Simon Hausmann <hausmann@kde.org> |
4 | SPDX-FileCopyrightText: 2000 Kurt Granroth <granroth@kde.org> |
5 | |
6 | SPDX-License-Identifier: LGPL-2.0-only |
7 | */ |
8 | |
9 | #include "kxmlguiclient.h" |
10 | |
11 | #include "debug.h" |
12 | #include "kactioncollection.h" |
13 | #include "kxmlguibuilder.h" |
14 | #include "kxmlguifactory.h" |
15 | #include "kxmlguiversionhandler_p.h" |
16 | |
17 | #include <QAction> |
18 | #include <QCoreApplication> |
19 | #include <QDir> |
20 | #include <QDomDocument> |
21 | #include <QFile> |
22 | #include <QPointer> |
23 | #include <QStandardPaths> |
24 | |
25 | #include <KAuthorized> |
26 | #include <KLocalizedString> |
27 | |
28 | #include <cassert> |
29 | |
30 | class KXMLGUIClientPrivate |
31 | { |
32 | public: |
33 | KXMLGUIClientPrivate() |
34 | : m_componentName(QCoreApplication::applicationName()) |
35 | , m_textTagNames({QStringLiteral("text" ), QStringLiteral("Text" ), QStringLiteral("title" )}) |
36 | { |
37 | } |
38 | ~KXMLGUIClientPrivate() |
39 | { |
40 | } |
41 | |
42 | bool mergeXML(QDomElement &base, QDomElement &additive, KActionCollection *actionCollection); |
43 | bool isEmptyContainer(const QDomElement &base, KActionCollection *actionCollection) const; |
44 | |
45 | QDomElement findMatchingElement(const QDomElement &base, const QDomElement &additive); |
46 | |
47 | QString m_componentName; |
48 | |
49 | QDomDocument m_doc; |
50 | KActionCollection *m_actionCollection = nullptr; |
51 | QDomDocument m_buildDocument; |
52 | QPointer<KXMLGUIFactory> m_factory; |
53 | KXMLGUIClient *m_parent = nullptr; |
54 | // QPtrList<KXMLGUIClient> m_supers; |
55 | QList<KXMLGUIClient *> m_children; |
56 | KXMLGUIBuilder *m_builder = nullptr; |
57 | QString m_xmlFile; |
58 | QString m_localXMLFile; |
59 | const QStringList m_textTagNames; |
60 | |
61 | // Actions to enable/disable on a state change |
62 | QMap<QString, KXMLGUIClient::StateChange> m_actionsStateMap; |
63 | }; |
64 | |
65 | KXMLGUIClient::KXMLGUIClient() |
66 | : d(new KXMLGUIClientPrivate) |
67 | { |
68 | } |
69 | |
70 | KXMLGUIClient::KXMLGUIClient(KXMLGUIClient *parent) |
71 | : d(new KXMLGUIClientPrivate) |
72 | { |
73 | Q_INIT_RESOURCE(kxmlgui); |
74 | |
75 | parent->insertChildClient(child: this); |
76 | } |
77 | |
78 | KXMLGUIClient::~KXMLGUIClient() |
79 | { |
80 | if (d->m_parent) { |
81 | d->m_parent->removeChildClient(child: this); |
82 | } |
83 | |
84 | if (d->m_factory) { |
85 | qCWarning(DEBUG_KXMLGUI) |
86 | << this << "deleted without having been removed from the factory first. This will leak standalone popupmenus and could lead to crashes." ; |
87 | d->m_factory->forgetClient(client: this); |
88 | } |
89 | |
90 | for (KXMLGUIClient *client : std::as_const(t&: d->m_children)) { |
91 | if (d->m_factory) { |
92 | d->m_factory->forgetClient(client); |
93 | } |
94 | assert(client->d->m_parent == this); |
95 | client->d->m_parent = nullptr; |
96 | } |
97 | |
98 | delete d->m_actionCollection; |
99 | } |
100 | |
101 | QAction *KXMLGUIClient::action(const QString &name) const |
102 | { |
103 | QAction *act = actionCollection()->action(name); |
104 | if (!act) { |
105 | for (KXMLGUIClient *client : std::as_const(t&: d->m_children)) { |
106 | act = client->actionCollection()->action(name); |
107 | if (act) { |
108 | break; |
109 | } |
110 | } |
111 | } |
112 | return act; |
113 | } |
114 | |
115 | KActionCollection *KXMLGUIClient::actionCollection() const |
116 | { |
117 | if (!d->m_actionCollection) { |
118 | d->m_actionCollection = new KActionCollection(this); |
119 | d->m_actionCollection->setObjectName(QStringLiteral("KXMLGUIClient-KActionCollection" )); |
120 | } |
121 | return d->m_actionCollection; |
122 | } |
123 | |
124 | QAction *KXMLGUIClient::action(const QDomElement &element) const |
125 | { |
126 | return actionCollection()->action(name: element.attribute(QStringLiteral("name" ))); |
127 | } |
128 | |
129 | QString KXMLGUIClient::componentName() const |
130 | { |
131 | return d->m_componentName; |
132 | } |
133 | |
134 | QDomDocument KXMLGUIClient::domDocument() const |
135 | { |
136 | return d->m_doc; |
137 | } |
138 | |
139 | QString KXMLGUIClient::xmlFile() const |
140 | { |
141 | return d->m_xmlFile; |
142 | } |
143 | |
144 | QString KXMLGUIClient::localXMLFile() const |
145 | { |
146 | if (!d->m_localXMLFile.isEmpty()) { |
147 | return d->m_localXMLFile; |
148 | } |
149 | |
150 | if (!QDir::isRelativePath(path: d->m_xmlFile)) { |
151 | return QString(); // can't save anything here |
152 | } |
153 | |
154 | if (d->m_xmlFile.isEmpty()) { // setXMLFile not called at all, can't save. Use case: ToolBarHandler |
155 | return QString(); |
156 | } |
157 | |
158 | return QStandardPaths::writableLocation(type: QStandardPaths::GenericDataLocation) + QLatin1String("/kxmlgui5/%1/%2" ).arg(args: componentName(), args&: d->m_xmlFile); |
159 | } |
160 | |
161 | void KXMLGUIClient::reloadXML() |
162 | { |
163 | // TODO: this method can't be used for the KXmlGuiWindow, since it doesn't merge in ui_standards.rc! |
164 | // -> KDE5: load ui_standards_rc in setXMLFile using a flag, and remember that flag? |
165 | // and then KEditToolBar can use reloadXML. |
166 | QString file(xmlFile()); |
167 | if (!file.isEmpty()) { |
168 | setXMLFile(file); |
169 | } |
170 | } |
171 | |
172 | void KXMLGUIClient::setComponentName(const QString &componentName, const QString &componentDisplayName) |
173 | { |
174 | d->m_componentName = componentName; |
175 | actionCollection()->setComponentName(componentName); |
176 | actionCollection()->setComponentDisplayName(componentDisplayName); |
177 | if (d->m_builder) { |
178 | d->m_builder->setBuilderClient(this); |
179 | } |
180 | } |
181 | |
182 | QString KXMLGUIClient::standardsXmlFileLocation() |
183 | { |
184 | if (QStandardPaths::isTestModeEnabled()) { |
185 | return QStringLiteral(":/kxmlgui5/ui_standards.rc" ); |
186 | } |
187 | QString file = QStandardPaths::locate(type: QStandardPaths::GenericConfigLocation, QStringLiteral("kxmlgui5/ui_standards.rc" )); |
188 | if (file.isEmpty()) { |
189 | // fallback to resource, to allow to use the rc file compiled into this framework, must exist! |
190 | file = QStringLiteral(":/kxmlgui5/ui_standards.rc" ); |
191 | Q_ASSERT(QFile::exists(file)); |
192 | } |
193 | return file; |
194 | } |
195 | |
196 | void KXMLGUIClient::loadStandardsXmlFile() |
197 | { |
198 | setXML(document: KXMLGUIFactory::readConfigFile(filename: standardsXmlFileLocation())); |
199 | } |
200 | |
201 | void KXMLGUIClient::setXMLFile(const QString &_file, bool merge, bool setXMLDoc) |
202 | { |
203 | // store our xml file name |
204 | if (!_file.isNull()) { |
205 | d->m_xmlFile = _file; |
206 | } |
207 | |
208 | if (!setXMLDoc) { |
209 | return; |
210 | } |
211 | |
212 | QString file = _file; |
213 | QStringList allFiles; |
214 | if (!QDir::isRelativePath(path: file)) { |
215 | allFiles.append(t: file); |
216 | } else { |
217 | const QString filter = componentName() + QLatin1Char('/') + _file; |
218 | |
219 | // files on filesystem |
220 | allFiles << QStandardPaths::locateAll(type: QStandardPaths::GenericDataLocation, QStringLiteral("kxmlgui5/" ) + filter); |
221 | |
222 | // built-in resource file |
223 | const QString qrcFile(QLatin1String(":/kxmlgui5/" ) + filter); |
224 | if (QFile::exists(fileName: qrcFile)) { |
225 | allFiles << qrcFile; |
226 | } |
227 | } |
228 | if (allFiles.isEmpty() && !_file.isEmpty()) { |
229 | // if a non-empty file gets passed and we can't find it, |
230 | // inform the developer using some debug output |
231 | qCWarning(DEBUG_KXMLGUI) << "cannot find .rc file" << _file << "for component" << componentName(); |
232 | } |
233 | |
234 | // make sure to merge the settings from any file specified by setLocalXMLFile() |
235 | if (!d->m_localXMLFile.isEmpty() && !file.endsWith(s: QLatin1String("ui_standards.rc" ))) { |
236 | const bool exists = QDir::isRelativePath(path: d->m_localXMLFile) || QFile::exists(fileName: d->m_localXMLFile); |
237 | if (exists && !allFiles.contains(str: d->m_localXMLFile)) { |
238 | allFiles.prepend(t: d->m_localXMLFile); |
239 | } |
240 | } |
241 | |
242 | QString doc; |
243 | if (!allFiles.isEmpty()) { |
244 | file = findMostRecentXMLFile(files: allFiles, doc); |
245 | } |
246 | |
247 | // Always call setXML, even on error, so that we don't keep all ui_standards.rc menus. |
248 | setXML(document: doc, merge); |
249 | } |
250 | |
251 | void KXMLGUIClient::setLocalXMLFile(const QString &file) |
252 | { |
253 | d->m_localXMLFile = file; |
254 | } |
255 | |
256 | void KXMLGUIClient::replaceXMLFile(const QString &xmlfile, const QString &localxmlfile, bool merge) |
257 | { |
258 | if (!QDir::isAbsolutePath(path: xmlfile)) { |
259 | qCWarning(DEBUG_KXMLGUI) << "xml file" << xmlfile << "is not an absolute path" ; |
260 | } |
261 | |
262 | setLocalXMLFile(localxmlfile); |
263 | setXMLFile(file: xmlfile, merge); |
264 | } |
265 | |
266 | // The top document element may have translation domain attribute set, |
267 | // or the translation domain may be implicitly the application domain. |
268 | // This domain must be used to fetch translations for all text elements |
269 | // in the document that do not have their own domain attribute. |
270 | // In order to preserve this semantics through document mergings, |
271 | // the top or application domain must be propagated to all text elements |
272 | // lacking their own domain attribute. |
273 | static void propagateTranslationDomain(QDomDocument &doc, const QStringList &tagNames) |
274 | { |
275 | const QLatin1String attrDomain("translationDomain" ); |
276 | QDomElement base = doc.documentElement(); |
277 | QString domain = base.attribute(name: attrDomain); |
278 | if (domain.isEmpty()) { |
279 | domain = QString::fromUtf8(ba: KLocalizedString::applicationDomain()); |
280 | if (domain.isEmpty()) { |
281 | return; |
282 | } |
283 | } |
284 | for (const QString &tagName : tagNames) { |
285 | QDomNodeList textNodes = base.elementsByTagName(tagname: tagName); |
286 | for (int i = 0; i < textNodes.length(); ++i) { |
287 | QDomElement e = textNodes.item(index: i).toElement(); |
288 | QString localDomain = e.attribute(name: attrDomain); |
289 | if (localDomain.isEmpty()) { |
290 | e.setAttribute(name: attrDomain, value: domain); |
291 | } |
292 | } |
293 | } |
294 | } |
295 | |
296 | void KXMLGUIClient::setXML(const QString &document, bool merge) |
297 | { |
298 | QDomDocument doc; |
299 | // QDomDocument raises a parse error on empty document, but we accept no app-specific document, |
300 | // in which case you only get ui_standards.rc layout. |
301 | if (!document.isEmpty()) { |
302 | const QDomDocument::ParseResult result = doc.setContent(data: document); |
303 | if (!result) { |
304 | qCCritical(DEBUG_KXMLGUI) << "Error parsing XML document:" << result.errorMessage << "at line" << result.errorLine << "column" |
305 | << result.errorColumn; |
306 | setDOMDocument(document: QDomDocument(), merge); // otherwise empty menus from ui_standards.rc stay around |
307 | return; |
308 | } |
309 | } |
310 | |
311 | propagateTranslationDomain(doc, tagNames: d->m_textTagNames); |
312 | setDOMDocument(document: doc, merge); |
313 | } |
314 | |
315 | void KXMLGUIClient::setDOMDocument(const QDomDocument &document, bool merge) |
316 | { |
317 | if (merge && !d->m_doc.isNull()) { |
318 | QDomElement base = d->m_doc.documentElement(); |
319 | |
320 | QDomElement e = document.documentElement(); |
321 | |
322 | // merge our original (global) xml with our new one |
323 | d->mergeXML(base, additive&: e, actionCollection: actionCollection()); |
324 | |
325 | // reassign our pointer as mergeXML might have done something |
326 | // strange to it |
327 | base = d->m_doc.documentElement(); |
328 | |
329 | // qCDebug(DEBUG_KXMLGUI) << "Result of xmlgui merging:" << d->m_doc.toString(); |
330 | |
331 | // we want some sort of failsafe.. just in case |
332 | if (base.isNull()) { |
333 | d->m_doc = document; |
334 | } |
335 | } else { |
336 | d->m_doc = document; |
337 | } |
338 | |
339 | setXMLGUIBuildDocument(QDomDocument()); |
340 | } |
341 | |
342 | // if (equals(a,b)) is more readable than if (a.compare(b, Qt::CaseInsensitive)==0) |
343 | static inline bool equalstr(const QString &a, const QString &b) |
344 | { |
345 | return a.compare(s: b, cs: Qt::CaseInsensitive) == 0; |
346 | } |
347 | static inline bool equalstr(const QString &a, QLatin1String b) |
348 | { |
349 | return a.compare(other: b, cs: Qt::CaseInsensitive) == 0; |
350 | } |
351 | |
352 | bool KXMLGUIClientPrivate::mergeXML(QDomElement &base, QDomElement &additive, KActionCollection *actionCollection) |
353 | { |
354 | const QLatin1String tagAction("Action" ); |
355 | const QLatin1String tagMerge("Merge" ); |
356 | const QLatin1String tagSeparator("Separator" ); |
357 | const QLatin1String tagMergeLocal("MergeLocal" ); |
358 | const QLatin1String tagText("text" ); |
359 | const QLatin1String attrAppend("append" ); |
360 | const QString attrName(QStringLiteral("name" )); |
361 | const QString attrWeakSeparator(QStringLiteral("weakSeparator" )); |
362 | const QString attrAlreadyVisited(QStringLiteral("alreadyVisited" )); |
363 | const QString attrNoMerge(QStringLiteral("noMerge" )); |
364 | const QLatin1String attrOne("1" ); |
365 | |
366 | // there is a possibility that we don't want to merge in the |
367 | // additive.. rather, we might want to *replace* the base with the |
368 | // additive. this can be for any container.. either at a file wide |
369 | // level or a simple container level. we look for the 'noMerge' |
370 | // tag, in any event and just replace the old with the new |
371 | if (additive.attribute(name: attrNoMerge) == attrOne) { // ### use toInt() instead? (Simon) |
372 | base.parentNode().replaceChild(newChild: additive, oldChild: base); |
373 | return true; |
374 | } else { |
375 | // Merge attributes |
376 | { |
377 | const QDomNamedNodeMap attribs = additive.attributes(); |
378 | const int attribcount = attribs.count(); |
379 | |
380 | for (int i = 0; i < attribcount; ++i) { |
381 | const QDomNode node = attribs.item(index: i); |
382 | base.setAttribute(name: node.nodeName(), value: node.nodeValue()); |
383 | } |
384 | } |
385 | |
386 | // iterate over all elements in the container (of the global DOM tree) |
387 | QDomNode n = base.firstChild(); |
388 | while (!n.isNull()) { |
389 | QDomElement e = n.toElement(); |
390 | n = n.nextSibling(); // Advance now so that we can safely delete e |
391 | if (e.isNull()) { |
392 | continue; |
393 | } |
394 | |
395 | const QString tag = e.tagName(); |
396 | |
397 | // if there's an action tag in the global tree and the action is |
398 | // not implemented, then we remove the element |
399 | if (equalstr(a: tag, b: tagAction)) { |
400 | const QString name = e.attribute(name: attrName); |
401 | if (!actionCollection->action(name) || !KAuthorized::authorizeAction(action: name)) { |
402 | // remove this child as we aren't using it |
403 | base.removeChild(oldChild: e); |
404 | continue; |
405 | } |
406 | } |
407 | |
408 | // if there's a separator defined in the global tree, then add an |
409 | // attribute, specifying that this is a "weak" separator |
410 | else if (equalstr(a: tag, b: tagSeparator)) { |
411 | e.setAttribute(name: attrWeakSeparator, value: uint(1)); |
412 | |
413 | // okay, hack time. if the last item was a weak separator OR |
414 | // this is the first item in a container, then we nuke the |
415 | // current one |
416 | QDomElement prev = e.previousSibling().toElement(); |
417 | if (prev.isNull() // |
418 | || (equalstr(a: prev.tagName(), b: tagSeparator) && !prev.attribute(name: attrWeakSeparator).isNull()) // |
419 | || (equalstr(a: prev.tagName(), b: tagText))) { |
420 | // the previous element was a weak separator or didn't exist |
421 | base.removeChild(oldChild: e); |
422 | continue; |
423 | } |
424 | } |
425 | |
426 | // the MergeLocal tag lets us specify where non-standard elements |
427 | // of the local tree shall be merged in. After inserting the |
428 | // elements we delete this element |
429 | else if (equalstr(a: tag, b: tagMergeLocal)) { |
430 | QDomNode it = additive.firstChild(); |
431 | while (!it.isNull()) { |
432 | QDomElement newChild = it.toElement(); |
433 | it = it.nextSibling(); |
434 | if (newChild.isNull()) { |
435 | continue; |
436 | } |
437 | |
438 | if (equalstr(a: newChild.tagName(), b: tagText)) { |
439 | continue; |
440 | } |
441 | |
442 | if (newChild.attribute(name: attrAlreadyVisited) == attrOne) { |
443 | continue; |
444 | } |
445 | |
446 | QString itAppend(newChild.attribute(name: attrAppend)); |
447 | QString elemName(e.attribute(name: attrName)); |
448 | |
449 | if ((itAppend.isNull() && elemName.isEmpty()) || (itAppend == elemName)) { |
450 | // first, see if this new element matches a standard one in |
451 | // the global file. if it does, then we skip it as it will |
452 | // be merged in, later |
453 | QDomElement matchingElement = findMatchingElement(base: newChild, additive: base); |
454 | if (matchingElement.isNull() || equalstr(a: newChild.tagName(), b: tagSeparator)) { |
455 | base.insertBefore(newChild, refChild: e); |
456 | } |
457 | } |
458 | } |
459 | |
460 | base.removeChild(oldChild: e); |
461 | continue; |
462 | } |
463 | |
464 | else if (equalstr(a: tag, b: tagText)) { |
465 | continue; |
466 | } else if (equalstr(a: tag, b: tagMerge)) { |
467 | continue; |
468 | } |
469 | |
470 | // in this last case we check for a separator tag and, if not, we |
471 | // can be sure that it is a container --> proceed with child nodes |
472 | // recursively and delete the just proceeded container item in |
473 | // case it is empty (if the recursive call returns true) |
474 | else { |
475 | QDomElement matchingElement = findMatchingElement(base: e, additive); |
476 | if (!matchingElement.isNull()) { |
477 | matchingElement.setAttribute(name: attrAlreadyVisited, value: uint(1)); |
478 | |
479 | if (mergeXML(base&: e, additive&: matchingElement, actionCollection)) { |
480 | base.removeChild(oldChild: e); |
481 | additive.removeChild(oldChild: matchingElement); // make sure we don't append it below |
482 | continue; |
483 | } |
484 | |
485 | continue; |
486 | } else { |
487 | // this is an important case here! We reach this point if the |
488 | // "local" tree does not contain a container definition for |
489 | // this container. However we have to call mergeXML recursively |
490 | // and make it check if there are actions implemented for this |
491 | // container. *If* none, then we can remove this container now |
492 | QDomElement dummy; |
493 | if (mergeXML(base&: e, additive&: dummy, actionCollection)) { |
494 | base.removeChild(oldChild: e); |
495 | } |
496 | continue; |
497 | } |
498 | } |
499 | } |
500 | |
501 | // here we append all child elements which were not inserted |
502 | // previously via the LocalMerge tag |
503 | n = additive.firstChild(); |
504 | while (!n.isNull()) { |
505 | QDomElement e = n.toElement(); |
506 | n = n.nextSibling(); // Advance now so that we can safely delete e |
507 | if (e.isNull()) { |
508 | continue; |
509 | } |
510 | |
511 | QDomElement matchingElement = findMatchingElement(base: e, additive: base); |
512 | |
513 | if (matchingElement.isNull()) { |
514 | base.appendChild(newChild: e); |
515 | } |
516 | } |
517 | |
518 | // do one quick check to make sure that the last element was not |
519 | // a weak separator |
520 | QDomElement last = base.lastChild().toElement(); |
521 | if (equalstr(a: last.tagName(), b: tagSeparator) && (!last.attribute(name: attrWeakSeparator).isNull())) { |
522 | base.removeChild(oldChild: last); |
523 | } |
524 | } |
525 | |
526 | return isEmptyContainer(base, actionCollection); |
527 | } |
528 | |
529 | bool KXMLGUIClientPrivate::isEmptyContainer(const QDomElement &base, KActionCollection *actionCollection) const |
530 | { |
531 | // now we check if we are empty (in which case we return "true", to |
532 | // indicate the caller that it can delete "us" (the base element |
533 | // argument of "this" call) |
534 | QDomNode n = base.firstChild(); |
535 | while (!n.isNull()) { |
536 | const QDomElement e = n.toElement(); |
537 | n = n.nextSibling(); // Advance now so that we can safely delete e |
538 | if (e.isNull()) { |
539 | continue; |
540 | } |
541 | |
542 | const QString tag = e.tagName(); |
543 | |
544 | if (equalstr(a: tag, b: QLatin1String("Action" ))) { |
545 | // if base contains an implemented action, then we must not get |
546 | // deleted (note that the actionCollection contains both, |
547 | // "global" and "local" actions) |
548 | if (actionCollection->action(name: e.attribute(QStringLiteral("name" )))) { |
549 | return false; |
550 | } |
551 | } else if (equalstr(a: tag, b: QLatin1String("Separator" ))) { |
552 | // if we have a separator which has *not* the weak attribute |
553 | // set, then it must be owned by the "local" tree in which case |
554 | // we must not get deleted either |
555 | const QString weakAttr = e.attribute(QStringLiteral("weakSeparator" )); |
556 | if (weakAttr.isEmpty() || weakAttr.toInt() != 1) { |
557 | return false; |
558 | } |
559 | } |
560 | |
561 | else if (equalstr(a: tag, b: QLatin1String("merge" ))) { |
562 | continue; |
563 | } |
564 | |
565 | // a text tag is NOT enough to spare this container |
566 | else if (equalstr(a: tag, b: QLatin1String("text" ))) { |
567 | continue; |
568 | } |
569 | |
570 | // what's left are non-empty containers! *don't* delete us in this |
571 | // case (at this position we can be *sure* that the container is |
572 | // *not* empty, as the recursive call for it was in the first loop |
573 | // which deleted the element in case the call returned "true" |
574 | else { |
575 | return false; |
576 | } |
577 | } |
578 | |
579 | return true; // I'm empty, please delete me. |
580 | } |
581 | |
582 | QDomElement KXMLGUIClientPrivate::findMatchingElement(const QDomElement &base, const QDomElement &additive) |
583 | { |
584 | const QString idAttribute(base.tagName() == QLatin1String("ActionProperties" ) ? QStringLiteral("scheme" ) : QStringLiteral("name" )); |
585 | |
586 | QDomNode n = additive.firstChild(); |
587 | while (!n.isNull()) { |
588 | QDomElement e = n.toElement(); |
589 | n = n.nextSibling(); // Advance now so that we can safely delete e -- TODO we don't, so simplify this |
590 | if (e.isNull()) { |
591 | continue; |
592 | } |
593 | |
594 | const QString tag = e.tagName(); |
595 | // skip all action and merge tags as we will never use them |
596 | if (equalstr(a: tag, b: QLatin1String("Action" )) // |
597 | || equalstr(a: tag, b: QLatin1String("MergeLocal" ))) { |
598 | continue; |
599 | } |
600 | |
601 | // now see if our tags are equivalent |
602 | if (equalstr(a: tag, b: base.tagName()) // |
603 | && e.attribute(name: idAttribute) == base.attribute(name: idAttribute)) { |
604 | return e; |
605 | } |
606 | } |
607 | |
608 | // nope, return a (now) null element |
609 | return QDomElement(); |
610 | } |
611 | |
612 | void KXMLGUIClient::setXMLGUIBuildDocument(const QDomDocument &doc) |
613 | { |
614 | d->m_buildDocument = doc; |
615 | } |
616 | |
617 | QDomDocument KXMLGUIClient::xmlguiBuildDocument() const |
618 | { |
619 | return d->m_buildDocument; |
620 | } |
621 | |
622 | void KXMLGUIClient::setFactory(KXMLGUIFactory *factory) |
623 | { |
624 | d->m_factory = factory; |
625 | } |
626 | |
627 | KXMLGUIFactory *KXMLGUIClient::factory() const |
628 | { |
629 | return d->m_factory; |
630 | } |
631 | |
632 | KXMLGUIClient *KXMLGUIClient::parentClient() const |
633 | { |
634 | return d->m_parent; |
635 | } |
636 | |
637 | void KXMLGUIClient::insertChildClient(KXMLGUIClient *child) |
638 | { |
639 | if (child->d->m_parent) { |
640 | child->d->m_parent->removeChildClient(child); |
641 | } |
642 | d->m_children.append(t: child); |
643 | child->d->m_parent = this; |
644 | } |
645 | |
646 | void KXMLGUIClient::removeChildClient(KXMLGUIClient *child) |
647 | { |
648 | assert(d->m_children.contains(child)); |
649 | d->m_children.removeAll(t: child); |
650 | child->d->m_parent = nullptr; |
651 | } |
652 | |
653 | /*bool KXMLGUIClient::addSuperClient( KXMLGUIClient *super ) |
654 | { |
655 | if ( d->m_supers.contains( super ) ) |
656 | return false; |
657 | d->m_supers.append( super ); |
658 | return true; |
659 | }*/ |
660 | |
661 | QList<KXMLGUIClient *> KXMLGUIClient::childClients() |
662 | { |
663 | return d->m_children; |
664 | } |
665 | |
666 | void KXMLGUIClient::setClientBuilder(KXMLGUIBuilder *builder) |
667 | { |
668 | d->m_builder = builder; |
669 | } |
670 | |
671 | KXMLGUIBuilder *KXMLGUIClient::clientBuilder() const |
672 | { |
673 | return d->m_builder; |
674 | } |
675 | |
676 | void KXMLGUIClient::plugActionList(const QString &name, const QList<QAction *> &actionList) |
677 | { |
678 | if (!d->m_factory) { |
679 | return; |
680 | } |
681 | |
682 | d->m_factory->plugActionList(client: this, name, actionList); |
683 | } |
684 | |
685 | void KXMLGUIClient::unplugActionList(const QString &name) |
686 | { |
687 | if (!d->m_factory) { |
688 | return; |
689 | } |
690 | |
691 | d->m_factory->unplugActionList(client: this, name); |
692 | } |
693 | |
694 | QString KXMLGUIClient::findMostRecentXMLFile(const QStringList &files, QString &doc) |
695 | { |
696 | KXmlGuiVersionHandler versionHandler(files); |
697 | doc = versionHandler.finalDocument(); |
698 | return versionHandler.finalFile(); |
699 | } |
700 | |
701 | void KXMLGUIClient::addStateActionEnabled(const QString &state, const QString &action) |
702 | { |
703 | StateChange stateChange = getActionsToChangeForState(state); |
704 | |
705 | stateChange.actionsToEnable.append(t: action); |
706 | // qCDebug(DEBUG_KXMLGUI) << "KXMLGUIClient::addStateActionEnabled( " << state << ", " << action << ")"; |
707 | |
708 | d->m_actionsStateMap.insert(key: state, value: stateChange); |
709 | } |
710 | |
711 | void KXMLGUIClient::addStateActionDisabled(const QString &state, const QString &action) |
712 | { |
713 | StateChange stateChange = getActionsToChangeForState(state); |
714 | |
715 | stateChange.actionsToDisable.append(t: action); |
716 | // qCDebug(DEBUG_KXMLGUI) << "KXMLGUIClient::addStateActionDisabled( " << state << ", " << action << ")"; |
717 | |
718 | d->m_actionsStateMap.insert(key: state, value: stateChange); |
719 | } |
720 | |
721 | KXMLGUIClient::StateChange KXMLGUIClient::getActionsToChangeForState(const QString &state) |
722 | { |
723 | return d->m_actionsStateMap[state]; |
724 | } |
725 | |
726 | void KXMLGUIClient::stateChanged(const QString &newstate, KXMLGUIClient::ReverseStateChange reverse) |
727 | { |
728 | const StateChange stateChange = getActionsToChangeForState(state: newstate); |
729 | |
730 | bool setTrue = (reverse == StateNoReverse); |
731 | bool setFalse = !setTrue; |
732 | |
733 | // Enable actions which need to be enabled... |
734 | // |
735 | for (const auto &actionId : stateChange.actionsToEnable) { |
736 | QAction *action = actionCollection()->action(name: actionId); |
737 | if (action) { |
738 | action->setEnabled(setTrue); |
739 | } |
740 | } |
741 | |
742 | // and disable actions which need to be disabled... |
743 | // |
744 | for (const auto &actionId : stateChange.actionsToDisable) { |
745 | QAction *action = actionCollection()->action(name: actionId); |
746 | if (action) { |
747 | action->setEnabled(setFalse); |
748 | } |
749 | } |
750 | } |
751 | |
752 | void KXMLGUIClient::beginXMLPlug(QWidget *w) |
753 | { |
754 | actionCollection()->addAssociatedWidget(widget: w); |
755 | for (KXMLGUIClient *client : std::as_const(t&: d->m_children)) { |
756 | client->beginXMLPlug(w); |
757 | } |
758 | } |
759 | |
760 | void KXMLGUIClient::endXMLPlug() |
761 | { |
762 | } |
763 | |
764 | void KXMLGUIClient::prepareXMLUnplug(QWidget *w) |
765 | { |
766 | actionCollection()->removeAssociatedWidget(widget: w); |
767 | for (KXMLGUIClient *client : std::as_const(t&: d->m_children)) { |
768 | client->prepareXMLUnplug(w); |
769 | } |
770 | } |
771 | |
772 | void KXMLGUIClient::virtual_hook(int, void *) |
773 | { |
774 | /*BASE::virtual_hook( id, data );*/ |
775 | } |
776 | |
777 | QString KXMLGUIClient::findVersionNumber(const QString &xml) |
778 | { |
779 | enum { |
780 | ST_START, |
781 | ST_AFTER_OPEN, |
782 | ST_AFTER_GUI, |
783 | ST_EXPECT_VERSION, |
784 | ST_VERSION_NUM, |
785 | } state = ST_START; |
786 | const int length = xml.length(); |
787 | for (int pos = 0; pos < length; pos++) { |
788 | switch (state) { |
789 | case ST_START: |
790 | if (xml[pos] == QLatin1Char('<')) { |
791 | state = ST_AFTER_OPEN; |
792 | } |
793 | break; |
794 | case ST_AFTER_OPEN: { |
795 | // Jump to gui.. |
796 | const int guipos = xml.indexOf(s: QLatin1String("gui" ), from: pos, cs: Qt::CaseInsensitive); |
797 | if (guipos == -1) { |
798 | return QString(); // Reject |
799 | } |
800 | |
801 | pos = guipos + 2; // Position at i, so we're moved ahead to the next character by the ++; |
802 | state = ST_AFTER_GUI; |
803 | break; |
804 | } |
805 | case ST_AFTER_GUI: |
806 | state = ST_EXPECT_VERSION; |
807 | break; |
808 | case ST_EXPECT_VERSION: { |
809 | const int verpos = xml.indexOf(s: QLatin1String("version" ), from: pos, cs: Qt::CaseInsensitive); |
810 | if (verpos == -1) { |
811 | return QString(); // Reject |
812 | } |
813 | pos = verpos + 7; // strlen("version") is 7 |
814 | while (xml.at(i: pos).isSpace()) { |
815 | ++pos; |
816 | } |
817 | if (xml.at(i: pos++) != QLatin1Char('=')) { |
818 | return QString(); // Reject |
819 | } |
820 | while (xml.at(i: pos).isSpace()) { |
821 | ++pos; |
822 | } |
823 | |
824 | state = ST_VERSION_NUM; |
825 | break; |
826 | } |
827 | case ST_VERSION_NUM: { |
828 | int endpos; |
829 | for (endpos = pos; endpos < length; endpos++) { |
830 | const ushort ch = xml[endpos].unicode(); |
831 | if (ch >= QLatin1Char('0') && ch <= QLatin1Char('9')) { |
832 | continue; // Number.. |
833 | } |
834 | if (ch == QLatin1Char('"')) { // End of parameter |
835 | break; |
836 | } else { // This shouldn't be here.. |
837 | endpos = length; |
838 | } |
839 | } |
840 | |
841 | if (endpos != pos && endpos < length) { |
842 | const QString matchCandidate = xml.mid(position: pos, n: endpos - pos); // Don't include " ". |
843 | return matchCandidate; |
844 | } |
845 | |
846 | state = ST_EXPECT_VERSION; // Try to match a well-formed version.. |
847 | break; |
848 | } // case.. |
849 | } // switch |
850 | } // for |
851 | |
852 | return QString(); |
853 | } |
854 | |