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