1/*
2 This file is part of the KDE libraries
3 SPDX-FileCopyrightText: 2003 Waldo Bastian <bastian@kde.org>
4
5 SPDX-License-Identifier: LGPL-2.0-only
6*/
7
8#include "kbuildsycocainterface_p.h"
9#include "kservicefactory_p.h"
10#include "sycocadebug.h"
11#include "vfolder_menu_p.h"
12
13#include <kservice.h>
14
15#include <QDebug>
16#include <QDir>
17#include <QDirIterator>
18#include <QFile>
19#include <QMap>
20#include <QStandardPaths>
21
22static void foldNode(QDomElement &docElem, QDomElement &e, QMap<QString, QDomElement> &dupeList, QString s = QString()) // krazy:exclude=passbyvalue
23{
24 if (s.isEmpty()) {
25 s = e.text();
26 }
27 auto it = dupeList.find(key: s);
28 if (it != dupeList.end()) {
29 // qCDebug(SYCOCA) << e.tagName() << "and" << s << "requires combining!";
30
31 docElem.removeChild(oldChild: *it);
32 dupeList.erase(it);
33 }
34 dupeList.insert(key: s, value: e);
35}
36
37static void replaceNode(QDomElement &docElem, QDomNode &node, const QStringList &list, const QString &tag)
38{
39 for (const QString &str : list) {
40 QDomElement element = docElem.ownerDocument().createElement(tagName: tag);
41 const QDomText txt = docElem.ownerDocument().createTextNode(data: str);
42 element.appendChild(newChild: txt);
43 docElem.insertAfter(newChild: element, refChild: node);
44 }
45
46 QDomNode next = node.nextSibling();
47 docElem.removeChild(oldChild: node);
48 node = next;
49 // qCDebug(SYCOCA) << "Next tag = " << n.toElement().tagName();
50}
51
52void VFolderMenu::registerFile(const QString &file)
53{
54 int i = file.lastIndexOf(c: QLatin1Char('/'));
55 if (i < 0) {
56 return;
57 }
58
59 QString dir = file.left(n: i + 1); // Include trailing '/'
60 registerDirectory(directory: dir);
61}
62
63void VFolderMenu::registerDirectory(const QString &directory)
64{
65 m_allDirectories.append(t: directory);
66}
67
68QStringList VFolderMenu::allDirectories()
69{
70 if (m_allDirectories.isEmpty()) {
71 return m_allDirectories;
72 }
73 m_allDirectories.sort();
74
75 QStringList::Iterator it = m_allDirectories.begin();
76 QString previous = *it++;
77 for (; it != m_allDirectories.end();) {
78#ifndef Q_OS_WIN
79 if ((*it).startsWith(s: previous))
80#else
81 if ((*it).startsWith(previous, Qt::CaseInsensitive))
82#endif
83 {
84 it = m_allDirectories.erase(pos: it);
85 } else {
86 previous = *it;
87 ++it;
88 }
89 }
90 return m_allDirectories;
91}
92
93static void track(const QString &menuId,
94 const QString &menuName,
95 const QHash<QString, KService::Ptr> &includeList,
96 const QHash<QString, KService::Ptr> &excludeList,
97 const QHash<QString, KService::Ptr> &itemList,
98 const QString &comment)
99{
100 if (itemList.contains(key: menuId)) {
101 printf(format: "%s: %s INCL %d EXCL %d\n",
102 qPrintable(menuName),
103 qPrintable(comment),
104 includeList.contains(key: menuId) ? 1 : 0,
105 excludeList.contains(key: menuId) ? 1 : 0);
106 }
107}
108
109void VFolderMenu::includeItems(QHash<QString, KService::Ptr> &items1, const QHash<QString, KService::Ptr> &items2)
110{
111 for (const KService::Ptr &p : items2) {
112 items1.insert(key: p->menuId(), value: p);
113 }
114}
115
116void VFolderMenu::matchItems(QHash<QString, KService::Ptr> &items1, const QHash<QString, KService::Ptr> &items2)
117{
118 const QHash<QString, KService::Ptr> tmpItems1 = items1;
119 for (const KService::Ptr &p : tmpItems1) {
120 QString id = p->menuId();
121 if (!items2.contains(key: id)) {
122 items1.remove(key: id);
123 }
124 }
125}
126
127void VFolderMenu::excludeItems(QHash<QString, KService::Ptr> &items1, const QHash<QString, KService::Ptr> &items2)
128{
129 for (const KService::Ptr &p : items2) {
130 items1.remove(key: p->menuId());
131 }
132}
133
134VFolderMenu::SubMenu *VFolderMenu::takeSubMenu(SubMenu *parentMenu, const QString &menuName)
135{
136 const int i = menuName.indexOf(c: QLatin1Char('/'));
137 const QString s1 = i > 0 ? menuName.left(n: i) : menuName;
138 const QString s2 = menuName.mid(position: i + 1);
139
140 // Look up menu
141 for (QList<SubMenu *>::Iterator it = parentMenu->subMenus.begin(); it != parentMenu->subMenus.end(); ++it) {
142 SubMenu *menu = *it;
143 if (menu->name == s1) {
144 if (i == -1) {
145 // Take it out
146 parentMenu->subMenus.erase(pos: it);
147 return menu;
148 } else {
149 return takeSubMenu(parentMenu: menu, menuName: s2);
150 }
151 }
152 }
153 return nullptr; // Not found
154}
155
156void VFolderMenu::mergeMenu(SubMenu *menu1, SubMenu *menu2, bool reversePriority)
157{
158 if (m_track) {
159 track(menuId: m_trackId, menuName: menu1->name, includeList: menu1->items, excludeList: menu1->excludeItems, itemList: menu2->items, QStringLiteral("Before MenuMerge w. %1 (incl)").arg(a: menu2->name));
160 track(menuId: m_trackId, menuName: menu1->name, includeList: menu1->items, excludeList: menu1->excludeItems, itemList: menu2->excludeItems, QStringLiteral("Before MenuMerge w. %1 (excl)").arg(a: menu2->name));
161 }
162 if (reversePriority) {
163 // Merge menu1 with menu2, menu1 takes precedent
164 excludeItems(items1&: menu2->items, items2: menu1->excludeItems);
165 includeItems(items1&: menu1->items, items2: menu2->items);
166 excludeItems(items1&: menu2->excludeItems, items2: menu1->items);
167 includeItems(items1&: menu1->excludeItems, items2: menu2->excludeItems);
168 } else {
169 // Merge menu1 with menu2, menu2 takes precedent
170 excludeItems(items1&: menu1->items, items2: menu2->excludeItems);
171 includeItems(items1&: menu1->items, items2: menu2->items);
172 includeItems(items1&: menu1->excludeItems, items2: menu2->excludeItems);
173 menu1->isDeleted = menu2->isDeleted;
174 }
175 while (!menu2->subMenus.isEmpty()) {
176 SubMenu *subMenu = menu2->subMenus.takeFirst();
177 insertSubMenu(parentMenu: menu1, menuName: subMenu->name, newMenu: subMenu, reversePriority);
178 }
179
180 if (reversePriority) {
181 // Merge menu1 with menu2, menu1 takes precedent
182 if (menu1->directoryFile.isEmpty()) {
183 menu1->directoryFile = menu2->directoryFile;
184 }
185 if (menu1->defaultLayoutNode.isNull()) {
186 menu1->defaultLayoutNode = menu2->defaultLayoutNode;
187 }
188 if (menu1->layoutNode.isNull()) {
189 menu1->layoutNode = menu2->layoutNode;
190 }
191 } else {
192 // Merge menu1 with menu2, menu2 takes precedent
193 if (!menu2->directoryFile.isEmpty()) {
194 menu1->directoryFile = menu2->directoryFile;
195 }
196 if (!menu2->defaultLayoutNode.isNull()) {
197 menu1->defaultLayoutNode = menu2->defaultLayoutNode;
198 }
199 if (!menu2->layoutNode.isNull()) {
200 menu1->layoutNode = menu2->layoutNode;
201 }
202 }
203
204 if (m_track) {
205 track(menuId: m_trackId, menuName: menu1->name, includeList: menu1->items, excludeList: menu1->excludeItems, itemList: menu2->items, QStringLiteral("After MenuMerge w. %1 (incl)").arg(a: menu2->name));
206 track(menuId: m_trackId, menuName: menu1->name, includeList: menu1->items, excludeList: menu1->excludeItems, itemList: menu2->excludeItems, QStringLiteral("After MenuMerge w. %1 (excl)").arg(a: menu2->name));
207 }
208
209 delete menu2;
210}
211
212void VFolderMenu::insertSubMenu(SubMenu *parentMenu, const QString &menuName, SubMenu *newMenu, bool reversePriority)
213{
214 const int i = menuName.indexOf(c: QLatin1Char('/'));
215 const QString s1 = menuName.left(n: i);
216 const QString s2 = menuName.mid(position: i + 1);
217
218 // Look up menu
219 for (SubMenu *menu : std::as_const(t&: parentMenu->subMenus)) {
220 if (menu->name == s1) {
221 if (i == -1) {
222 mergeMenu(menu1: menu, menu2: newMenu, reversePriority);
223 return;
224 } else {
225 insertSubMenu(parentMenu: menu, menuName: s2, newMenu, reversePriority);
226 return;
227 }
228 }
229 }
230 if (i == -1) {
231 // Add it here
232 newMenu->name = menuName;
233 parentMenu->subMenus.append(t: newMenu);
234 } else {
235 SubMenu *menu = new SubMenu;
236 menu->name = s1;
237 parentMenu->subMenus.append(t: menu);
238 insertSubMenu(parentMenu: menu, menuName: s2, newMenu);
239 }
240}
241
242void VFolderMenu::insertService(SubMenu *parentMenu, const QString &name, KService::Ptr newService)
243{
244 const int i = name.indexOf(c: QLatin1Char('/'));
245
246 if (i == -1) {
247 // Add it here
248 parentMenu->items.insert(key: newService->menuId(), value: newService);
249 return;
250 }
251
252 QString s1 = name.left(n: i);
253 QString s2 = name.mid(position: i + 1);
254
255 // Look up menu
256 for (SubMenu *menu : std::as_const(t&: parentMenu->subMenus)) {
257 if (menu->name == s1) {
258 insertService(parentMenu: menu, name: s2, newService);
259 return;
260 }
261 }
262
263 SubMenu *menu = new SubMenu;
264 menu->name = s1;
265 parentMenu->subMenus.append(t: menu);
266 insertService(parentMenu: menu, name: s2, newService);
267}
268
269VFolderMenu::VFolderMenu(KServiceFactory *serviceFactory, KBuildSycocaInterface *kbuildsycocaInterface)
270 : m_appsInfo(nullptr)
271 , m_rootMenu(nullptr)
272 , m_currentMenu(nullptr)
273 , m_track(false)
274 , m_serviceFactory(serviceFactory)
275 , m_kbuildsycocaInterface(kbuildsycocaInterface)
276{
277 m_usedAppsDict.reserve(asize: 797);
278 initDirs();
279}
280
281VFolderMenu::~VFolderMenu()
282{
283 qDeleteAll(c: m_appsInfoList);
284 delete m_rootMenu;
285}
286// clang-format off
287#define FOR_ALL_APPLICATIONS(it) \
288 for (AppsInfo *info : std::as_const(m_appsInfoStack)) \
289 { \
290 QHashIterator<QString,KService::Ptr> it = info->applications; \
291 while (it.hasNext()) \
292 { \
293 it.next();
294#define FOR_ALL_APPLICATIONS_END } }
295
296#define FOR_CATEGORY(category, it) \
297 for (AppsInfo *info : std::as_const(m_appsInfoStack)) \
298 { \
299 const KService::List list = info->dictCategories.value(category); \
300 for(KService::List::ConstIterator it = list.constBegin(); \
301 it != list.constEnd(); ++it) \
302 {
303#define FOR_CATEGORY_END } }
304// clang-format on
305
306KService::Ptr VFolderMenu::findApplication(const QString &relPath)
307{
308 for (AppsInfo *info : std::as_const(t&: m_appsInfoStack)) {
309 if (info->applications.contains(key: relPath)) {
310 KService::Ptr s = info->applications[relPath];
311 if (s) {
312 return s;
313 }
314 }
315 }
316 return KService::Ptr();
317}
318
319void VFolderMenu::addApplication(const QString &id, KService::Ptr service)
320{
321 service->setMenuId(id);
322 m_appsInfo->applications.insert(key: id, value: service); // replaces, if already there
323 m_serviceFactory->addEntry(newEntry: KSycocaEntry::Ptr(service));
324}
325
326void VFolderMenu::buildApplicationIndex(bool unusedOnly)
327{
328 for (AppsInfo *info : std::as_const(t&: m_appsInfoList)) {
329 info->dictCategories.clear();
330 QMutableHashIterator<QString, KService::Ptr> it = info->applications;
331 while (it.hasNext()) {
332 KService::Ptr s = it.next().value();
333 if (unusedOnly && m_usedAppsDict.contains(value: s->menuId())) {
334 // Remove and skip this one
335 it.remove();
336 continue;
337 }
338
339 const auto categories = s->categories();
340 for (const QString &cat : categories) {
341 info->dictCategories[cat].append(t: s); // find or insert entry in hash
342 }
343 }
344 }
345}
346
347void VFolderMenu::createAppsInfo()
348{
349 if (m_appsInfo) {
350 return;
351 }
352
353 m_appsInfo = new AppsInfo;
354 m_appsInfoStack.prepend(t: m_appsInfo);
355 m_appsInfoList.append(t: m_appsInfo);
356 m_currentMenu->apps_info = m_appsInfo;
357}
358
359void VFolderMenu::loadAppsInfo()
360{
361 m_appsInfo = m_currentMenu->apps_info;
362 if (!m_appsInfo) {
363 return; // No appsInfo for this menu
364 }
365
366 if (!m_appsInfoStack.isEmpty() && m_appsInfoStack.first() == m_appsInfo) {
367 return; // Already added (By createAppsInfo?)
368 }
369
370 m_appsInfoStack.prepend(t: m_appsInfo); // Add
371}
372
373void VFolderMenu::unloadAppsInfo()
374{
375 m_appsInfo = m_currentMenu->apps_info;
376 if (!m_appsInfo) {
377 return; // No appsInfo for this menu
378 }
379
380 if (m_appsInfoStack.first() != m_appsInfo) {
381 return; // Already removed (huh?)
382 }
383
384 m_appsInfoStack.removeAll(t: m_appsInfo); // Remove
385 m_appsInfo = nullptr;
386}
387
388QString VFolderMenu::absoluteDir(const QString &_dir, const QString &baseDir, bool keepRelativeToCfg)
389{
390 QString dir = _dir;
391 if (QDir::isRelativePath(path: dir)) {
392 dir = baseDir + dir;
393 }
394
395 bool relative = QDir::isRelativePath(path: dir);
396 if (relative && !keepRelativeToCfg) {
397 relative = false;
398 dir = QStandardPaths::locate(type: QStandardPaths::GenericConfigLocation, fileName: QLatin1String("menus/") + dir, options: QStandardPaths::LocateDirectory);
399 }
400
401 if (!relative) {
402 QString resolved = QDir(dir).canonicalPath();
403 if (!resolved.isEmpty()) {
404 dir = resolved;
405 }
406 }
407
408 if (!dir.endsWith(c: QLatin1Char('/'))) {
409 dir += QLatin1Char('/');
410 }
411
412 return dir;
413}
414
415static void tagBaseDir(QDomDocument &doc, const QString &tag, const QString &dir)
416{
417 QDomNodeList mergeFileList = doc.elementsByTagName(tagname: tag);
418 for (int i = 0; i < mergeFileList.count(); i++) {
419 QDomAttr attr = doc.createAttribute(QStringLiteral("__BaseDir"));
420 attr.setValue(dir);
421 mergeFileList.item(index: i).toElement().setAttributeNode(attr);
422 }
423}
424
425static void tagBasePath(QDomDocument &doc, const QString &tag, const QString &path)
426{
427 QDomNodeList mergeFileList = doc.elementsByTagName(tagname: tag);
428 for (int i = 0; i < mergeFileList.count(); i++) {
429 QDomAttr attr = doc.createAttribute(QStringLiteral("__BasePath"));
430 attr.setValue(path);
431 mergeFileList.item(index: i).toElement().setAttributeNode(attr);
432 }
433}
434
435QDomDocument VFolderMenu::loadDoc()
436{
437 QDomDocument doc;
438 if (m_docInfo.path.isEmpty()) {
439 return doc;
440 }
441 QFile file(m_docInfo.path);
442 if (!file.open(flags: QIODevice::ReadOnly)) {
443 qCWarning(SYCOCA) << "Could not open " << m_docInfo.path;
444 return doc;
445 }
446 if (file.size() == 0) {
447 return doc;
448 }
449 const auto result = doc.setContent(device: &file);
450 if (!result) {
451 qCWarning(SYCOCA) << "Parse error in " << m_docInfo.path << ", line " << result.errorLine << ", col " << result.errorColumn << ": "
452 << result.errorMessage;
453 file.close();
454 return doc;
455 }
456 file.close();
457
458 tagBaseDir(doc, QStringLiteral("MergeFile"), dir: m_docInfo.baseDir);
459 tagBasePath(doc, QStringLiteral("MergeFile"), path: m_docInfo.path);
460 tagBaseDir(doc, QStringLiteral("MergeDir"), dir: m_docInfo.baseDir);
461 tagBaseDir(doc, QStringLiteral("DirectoryDir"), dir: m_docInfo.baseDir);
462 tagBaseDir(doc, QStringLiteral("AppDir"), dir: m_docInfo.baseDir);
463 tagBaseDir(doc, QStringLiteral("LegacyDir"), dir: m_docInfo.baseDir);
464
465 return doc;
466}
467
468void VFolderMenu::mergeFile(QDomElement &parent, const QDomNode &mergeHere)
469{
470 // qCDebug(SYCOCA) << m_docInfo.path;
471 QDomDocument doc = loadDoc();
472
473 QDomElement docElem = doc.documentElement();
474 QDomNode n = docElem.firstChild();
475 QDomNode last = mergeHere;
476 while (!n.isNull()) {
477 QDomElement e = n.toElement(); // try to convert the node to an element.
478 QDomNode next = n.nextSibling();
479
480 if (e.isNull()) {
481 // Skip
482 }
483 // The spec says we must ignore any Name nodes
484 else if (e.tagName() != QLatin1String("Name")) {
485 parent.insertAfter(newChild: n, refChild: last);
486 last = n;
487 }
488
489 docElem.removeChild(oldChild: n);
490 n = next;
491 }
492}
493
494void VFolderMenu::mergeMenus(QDomElement &docElem, QString &name)
495{
496 QMap<QString, QDomElement> menuNodes;
497 QMap<QString, QDomElement> directoryNodes;
498 QMap<QString, QDomElement> appDirNodes;
499 QMap<QString, QDomElement> directoryDirNodes;
500 QMap<QString, QDomElement> legacyDirNodes;
501 QDomElement defaultLayoutNode;
502 QDomElement layoutNode;
503
504 QDomNode n = docElem.firstChild();
505 while (!n.isNull()) {
506 QDomElement e = n.toElement(); // try to convert the node to an element.
507 if (e.isNull()) {
508 // qCDebug(SYCOCA) << "Empty node";
509 } else if (e.tagName() == QLatin1String("DefaultAppDirs")) {
510 // Replace with m_defaultAppDirs
511 replaceNode(docElem, node&: n, list: m_defaultAppDirs, QStringLiteral("AppDir"));
512 continue;
513 } else if (e.tagName() == QLatin1String("DefaultDirectoryDirs")) {
514 // Replace with m_defaultDirectoryDirs
515 replaceNode(docElem, node&: n, list: m_defaultDirectoryDirs, QStringLiteral("DirectoryDir"));
516 continue;
517 } else if (e.tagName() == QLatin1String("DefaultMergeDirs")) {
518 // Replace with m_defaultMergeDirs
519 replaceNode(docElem, node&: n, list: m_defaultMergeDirs, QStringLiteral("MergeDir"));
520 continue;
521 } else if (e.tagName() == QLatin1String("AppDir")) {
522 // Filter out dupes
523 foldNode(docElem, e, dupeList&: appDirNodes);
524 } else if (e.tagName() == QLatin1String("DirectoryDir")) {
525 // Filter out dupes
526 foldNode(docElem, e, dupeList&: directoryDirNodes);
527 } else if (e.tagName() == QLatin1String("LegacyDir")) {
528 // Filter out dupes
529 foldNode(docElem, e, dupeList&: legacyDirNodes);
530 } else if (e.tagName() == QLatin1String("Directory")) {
531 // Filter out dupes
532 foldNode(docElem, e, dupeList&: directoryNodes);
533 } else if (e.tagName() == QLatin1String("Move")) {
534 // Filter out dupes
535 QString orig;
536 QDomNode n2 = e.firstChild();
537 while (!n2.isNull()) {
538 QDomElement e2 = n2.toElement(); // try to convert the node to an element.
539 if (e2.tagName() == QLatin1String("Old")) {
540 orig = e2.text();
541 break;
542 }
543 n2 = n2.nextSibling();
544 }
545 foldNode(docElem, e, dupeList&: appDirNodes, s: orig);
546 } else if (e.tagName() == QLatin1String("Menu")) {
547 QString name;
548 mergeMenus(docElem&: e, name);
549 QMap<QString, QDomElement>::iterator it = menuNodes.find(key: name);
550 if (it != menuNodes.end()) {
551 QDomElement docElem2 = *it;
552 QDomNode n2 = docElem2.firstChild();
553 QDomNode first = e.firstChild();
554 while (!n2.isNull()) {
555 QDomElement e2 = n2.toElement(); // try to convert the node to an element.
556 QDomNode n3 = n2.nextSibling();
557 e.insertBefore(newChild: n2, refChild: first);
558 docElem2.removeChild(oldChild: n2);
559 n2 = n3;
560 }
561 // We still have duplicated Name entries
562 // but we don't care about that
563
564 docElem.removeChild(oldChild: docElem2);
565 menuNodes.erase(it);
566 }
567 menuNodes.insert(key: name, value: e);
568 } else if (e.tagName() == QLatin1String("MergeFile")) {
569 if ((e.attribute(QStringLiteral("type")) == QLatin1String("parent"))) {
570 // Ignore e.text(), as per the standard. We'll just look up the parent (more global) xml file.
571 pushDocInfoParent(basePath: e.attribute(QStringLiteral("__BasePath")), baseDir: e.attribute(QStringLiteral("__BaseDir")));
572 } else {
573 pushDocInfo(fileName: e.text(), baseDir: e.attribute(QStringLiteral("__BaseDir")));
574 }
575
576 if (!m_docInfo.path.isEmpty()) {
577 mergeFile(parent&: docElem, mergeHere: n);
578 }
579 popDocInfo();
580
581 QDomNode last = n;
582 n = n.nextSibling();
583 docElem.removeChild(oldChild: last); // Remove the MergeFile node
584 continue;
585 } else if (e.tagName() == QLatin1String("MergeDir")) {
586 const QString dir = absoluteDir(dir: e.text(), baseDir: e.attribute(QStringLiteral("__BaseDir")), keepRelativeToCfg: true);
587 Q_ASSERT(dir.endsWith(QLatin1Char('/')));
588
589 const bool relative = QDir::isRelativePath(path: dir);
590 const QStringList dirs =
591 QStandardPaths::locateAll(type: QStandardPaths::GenericConfigLocation, fileName: QLatin1String("menus/") + dir, options: QStandardPaths::LocateDirectory);
592 for (const QString &menuDir : dirs) {
593 registerDirectory(directory: menuDir);
594 }
595
596 QStringList fileList;
597 for (const QString &menuDir : dirs) {
598 const QStringList fileNames = QDir(menuDir).entryList(nameFilters: QStringList() << QStringLiteral("*.menu"));
599 for (const QString &file : fileNames) {
600 const QString fileToAdd = relative ? dir + file : menuDir + file;
601 if (!fileList.contains(str: fileToAdd)) {
602 fileList.append(t: fileToAdd);
603 }
604 }
605 }
606
607 for (const QString &file : std::as_const(t&: fileList)) {
608 pushDocInfo(fileName: file);
609 mergeFile(parent&: docElem, mergeHere: n);
610 popDocInfo();
611 }
612
613 QDomNode last = n;
614 n = n.nextSibling();
615 docElem.removeChild(oldChild: last); // Remove the MergeDir node
616
617 continue;
618 } else if (e.tagName() == QLatin1String("Name")) {
619 name = e.text();
620 } else if (e.tagName() == QLatin1String("DefaultLayout")) {
621 if (!defaultLayoutNode.isNull()) {
622 docElem.removeChild(oldChild: defaultLayoutNode);
623 }
624 defaultLayoutNode = e;
625 } else if (e.tagName() == QLatin1String("Layout")) {
626 if (!layoutNode.isNull()) {
627 docElem.removeChild(oldChild: layoutNode);
628 }
629 layoutNode = e;
630 }
631 n = n.nextSibling();
632 }
633}
634
635static QString makeRelative(const QString &dir)
636{
637 const QString canonical = QDir(dir).canonicalPath();
638 const auto list = QStandardPaths::locateAll(type: QStandardPaths::GenericConfigLocation, QStringLiteral("menus"), options: QStandardPaths::LocateDirectory);
639 for (const QString &base : list) {
640 if (canonical.startsWith(s: base)) {
641 return canonical.mid(position: base.length() + 1);
642 }
643 }
644 return dir;
645}
646
647void VFolderMenu::pushDocInfo(const QString &fileName, const QString &baseDir)
648{
649 m_docInfoStack.push(t: m_docInfo);
650 if (!baseDir.isEmpty()) {
651 if (!QDir::isRelativePath(path: baseDir)) {
652 m_docInfo.baseDir = makeRelative(dir: baseDir);
653 } else {
654 m_docInfo.baseDir = baseDir;
655 }
656 }
657
658 QString baseName = fileName;
659 if (!QDir::isRelativePath(path: baseName)) {
660 registerFile(file: baseName);
661 } else {
662 baseName = m_docInfo.baseDir + baseName;
663 }
664
665 m_docInfo.path = locateMenuFile(fileName);
666 if (m_docInfo.path.isEmpty()) {
667 m_docInfo.baseDir.clear();
668 m_docInfo.baseName.clear();
669 qCDebug(SYCOCA) << "Menu" << fileName << "not found.";
670 return;
671 }
672 qCDebug(SYCOCA) << "Found menu file" << m_docInfo.path;
673 int i;
674 i = baseName.lastIndexOf(c: QLatin1Char('/'));
675 if (i > 0) {
676 m_docInfo.baseDir = baseName.left(n: i + 1);
677 m_docInfo.baseName = baseName.mid(position: i + 1, n: baseName.length() - i - 6);
678 } else {
679 m_docInfo.baseDir.clear();
680 m_docInfo.baseName = baseName.left(n: baseName.length() - 5);
681 }
682}
683
684void VFolderMenu::pushDocInfoParent(const QString &basePath, const QString &baseDir)
685{
686 m_docInfoStack.push(t: m_docInfo);
687
688 m_docInfo.baseDir = baseDir;
689
690 QString fileName = basePath.mid(position: basePath.lastIndexOf(c: QLatin1Char('/')) + 1);
691 m_docInfo.baseName = fileName.left(n: fileName.length() - 5); // without ".menu"
692 QString baseName = QDir::cleanPath(path: m_docInfo.baseDir + fileName);
693
694 QStringList result = QStandardPaths::locateAll(type: QStandardPaths::GenericConfigLocation, fileName: QLatin1String("menus/") + baseName);
695
696 // Remove anything "more local" than basePath.
697 while (!result.isEmpty() && (result.at(i: 0) != basePath)) {
698 result.removeFirst();
699 }
700
701 if (result.count() <= 1) {
702 m_docInfo.path.clear(); // No parent found
703 return;
704 }
705 // Now result.at(0) == basePath, take the next one, i.e. the one in the parent dir.
706 m_docInfo.path = result.at(i: 1);
707}
708
709void VFolderMenu::popDocInfo()
710{
711 m_docInfo = m_docInfoStack.pop();
712}
713
714QString VFolderMenu::locateMenuFile(const QString &fileName)
715{
716 if (!QDir::isRelativePath(path: fileName)) {
717 if (QFile::exists(fileName)) {
718 return fileName;
719 }
720 return QString();
721 }
722
723 QString result;
724
725 QString xdgMenuPrefix = QString::fromLocal8Bit(ba: qgetenv(varName: "XDG_MENU_PREFIX"));
726 if (!xdgMenuPrefix.isEmpty()) {
727 QFileInfo fileInfo(fileName);
728
729 QString fileNameOnly = fileInfo.fileName();
730 if (!fileNameOnly.startsWith(s: xdgMenuPrefix)) {
731 fileNameOnly = xdgMenuPrefix + fileNameOnly;
732 }
733
734 QString baseName = QDir::cleanPath(path: m_docInfo.baseDir + fileInfo.path() + QLatin1Char('/') + fileNameOnly);
735 result = QStandardPaths::locate(type: QStandardPaths::GenericConfigLocation, fileName: QLatin1String("menus/") + baseName);
736 }
737
738 if (result.isEmpty()) {
739 QString baseName = QDir::cleanPath(path: m_docInfo.baseDir + fileName);
740 result = QStandardPaths::locate(type: QStandardPaths::GenericConfigLocation, fileName: QLatin1String("menus/") + baseName);
741 }
742
743 return result;
744}
745
746QString VFolderMenu::locateDirectoryFile(const QString &fileName)
747{
748 if (fileName.isEmpty()) {
749 return QString();
750 }
751
752 if (!QDir::isRelativePath(path: fileName)) {
753 if (QFile::exists(fileName)) {
754 return fileName;
755 }
756 return QString();
757 }
758
759 // First location in the list wins
760 for (QStringList::ConstIterator it = m_directoryDirs.constBegin(); it != m_directoryDirs.constEnd(); ++it) {
761 QString tmp = (*it) + fileName;
762 if (QFile::exists(fileName: tmp)) {
763 return tmp;
764 }
765 }
766
767 return QString();
768}
769
770void VFolderMenu::initDirs()
771{
772 m_defaultAppDirs = QStandardPaths::standardLocations(type: QStandardPaths::ApplicationsLocation);
773 m_defaultDirectoryDirs =
774 QStandardPaths::locateAll(type: QStandardPaths::GenericDataLocation, QStringLiteral("desktop-directories"), options: QStandardPaths::LocateDirectory);
775}
776
777void VFolderMenu::loadMenu(const QString &fileName)
778{
779 m_defaultMergeDirs.clear();
780
781 if (!fileName.endsWith(s: QLatin1String(".menu"))) {
782 return;
783 }
784
785 pushDocInfo(fileName);
786 m_defaultMergeDirs << QStringLiteral("applications-merged/");
787 m_doc = loadDoc();
788 popDocInfo();
789
790 if (m_doc.isNull()) {
791 if (m_docInfo.path.isEmpty()) {
792 qCritical() << fileName << " not found in " << m_allDirectories;
793 } else {
794 qCWarning(SYCOCA) << "Load error (" << m_docInfo.path << ")";
795 }
796 return;
797 }
798
799 QDomElement e = m_doc.documentElement();
800 QString name;
801 mergeMenus(docElem&: e, name);
802}
803
804void VFolderMenu::processCondition(QDomElement &domElem, QHash<QString, KService::Ptr> &items)
805{
806 if (domElem.tagName() == QLatin1String("And")) {
807 QDomNode n = domElem.firstChild();
808 // Look for the first child element
809 while (!n.isNull()) { // loop in case of comments
810 QDomElement e = n.toElement();
811 n = n.nextSibling();
812 if (!e.isNull()) {
813 processCondition(domElem&: e, items);
814 break; // we only want the first one
815 }
816 }
817
818 QHash<QString, KService::Ptr> andItems;
819 while (!n.isNull()) {
820 QDomElement e = n.toElement();
821 if (e.tagName() == QLatin1String("Not")) {
822 // Special handling for "and not"
823 QDomNode n2 = e.firstChild();
824 while (!n2.isNull()) {
825 QDomElement e2 = n2.toElement();
826 andItems.clear();
827 processCondition(domElem&: e2, items&: andItems);
828 excludeItems(items1&: items, items2: andItems);
829 n2 = n2.nextSibling();
830 }
831 } else {
832 andItems.clear();
833 processCondition(domElem&: e, items&: andItems);
834 matchItems(items1&: items, items2: andItems);
835 }
836 n = n.nextSibling();
837 }
838 } else if (domElem.tagName() == QLatin1String("Or")) {
839 QDomNode n = domElem.firstChild();
840 // Look for the first child element
841 while (!n.isNull()) { // loop in case of comments
842 QDomElement e = n.toElement();
843 n = n.nextSibling();
844 if (!e.isNull()) {
845 processCondition(domElem&: e, items);
846 break; // we only want the first one
847 }
848 }
849
850 QHash<QString, KService::Ptr> orItems;
851 while (!n.isNull()) {
852 QDomElement e = n.toElement();
853 if (!e.isNull()) {
854 orItems.clear();
855 processCondition(domElem&: e, items&: orItems);
856 includeItems(items1&: items, items2: orItems);
857 }
858 n = n.nextSibling();
859 }
860 } else if (domElem.tagName() == QLatin1String("Not")) {
861 FOR_ALL_APPLICATIONS(it)
862 {
863 KService::Ptr s = it.value();
864 items.insert(key: s->menuId(), value: s);
865 }
866 FOR_ALL_APPLICATIONS_END
867
868 QHash<QString, KService::Ptr> notItems;
869 QDomNode n = domElem.firstChild();
870 while (!n.isNull()) {
871 QDomElement e = n.toElement();
872 if (!e.isNull()) {
873 notItems.clear();
874 processCondition(domElem&: e, items&: notItems);
875 excludeItems(items1&: items, items2: notItems);
876 }
877 n = n.nextSibling();
878 }
879 } else if (domElem.tagName() == QLatin1String("Category")) {
880 FOR_CATEGORY(domElem.text(), it)
881 {
882 KService::Ptr s = *it;
883 items.insert(key: s->menuId(), value: s);
884 }
885 FOR_CATEGORY_END
886 } else if (domElem.tagName() == QLatin1String("All")) {
887 FOR_ALL_APPLICATIONS(it)
888 {
889 KService::Ptr s = it.value();
890 items.insert(key: s->menuId(), value: s);
891 }
892 FOR_ALL_APPLICATIONS_END
893 } else if (domElem.tagName() == QLatin1String("Filename")) {
894 const QString filename = domElem.text();
895 // qCDebug(SYCOCA) << "Adding file" << filename;
896 KService::Ptr s = findApplication(relPath: filename);
897 if (s) {
898 items.insert(key: filename, value: s);
899 }
900 }
901}
902
903void VFolderMenu::loadApplications(const QString &dir, const QString &prefix)
904{
905 qCDebug(SYCOCA) << "Looking up applications under" << dir;
906
907 QDirIterator it(dir);
908 while (it.hasNext()) {
909 it.next();
910 const QFileInfo fi = it.fileInfo();
911 const QString fn = fi.fileName();
912 if (fi.isDir() && !fi.isSymLink() && !fi.isBundle()) { // same check as in ksycocautils_p.h
913 if (fn == QLatin1String(".") || fn == QLatin1String("..")) {
914 continue;
915 }
916 loadApplications(dir: fi.filePath(), prefix: prefix + fn + QLatin1Char('-'));
917 continue;
918 }
919 if (fi.isFile()) {
920 if (!fn.endsWith(s: QLatin1String(".desktop"))) {
921 continue;
922 }
923 KService::Ptr service = m_kbuildsycocaInterface->createService(path: fi.absoluteFilePath());
924#ifndef NDEBUG
925 if (fn.contains(s: QLatin1String("fake"))) {
926 qCDebug(SYCOCA) << "createService" << fi.absoluteFilePath() << "returned" << (service ? "valid service" : "NULL SERVICE");
927 }
928#endif
929 if (service) {
930 addApplication(id: prefix + fn, service);
931 }
932 }
933 }
934}
935
936void VFolderMenu::processLegacyDir(const QString &dir, const QString &relDir, const QString &prefix)
937{
938 // qCDebug(SYCOCA).nospace() << "processLegacyDir(" << dir << ", " << relDir << ", " << prefix << ")";
939
940 QHash<QString, KService::Ptr> items;
941 QDirIterator it(dir);
942 while (it.hasNext()) {
943 it.next();
944 const QFileInfo fi = it.fileInfo();
945 const QString fn = fi.fileName();
946 if (fi.isDir()) {
947 if (fn == QLatin1String(".") || fn == QLatin1String("..")) {
948 continue;
949 }
950 SubMenu *parentMenu = m_currentMenu;
951
952 m_currentMenu = new SubMenu;
953 m_currentMenu->name = fn;
954 m_currentMenu->directoryFile = fi.absoluteFilePath() + QLatin1String("/.directory");
955
956 parentMenu->subMenus.append(t: m_currentMenu);
957
958 processLegacyDir(dir: fi.filePath(), relDir: relDir + fn + QLatin1Char('/'), prefix);
959 m_currentMenu = parentMenu;
960 continue;
961 }
962 if (fi.isFile() /*&& !fi.isSymLink() ?? */) {
963 if (!fn.endsWith(s: QLatin1String(".desktop"))) {
964 continue;
965 }
966 KService::Ptr service = m_kbuildsycocaInterface->createService(path: fi.absoluteFilePath());
967 if (service) {
968 const QString id = prefix + fn;
969
970 // TODO: Add legacy category
971 addApplication(id, service);
972 items.insert(key: service->menuId(), value: service);
973
974 if (service->categories().isEmpty()) {
975 m_currentMenu->items.insert(key: id, value: service);
976 }
977 }
978 }
979 }
980 markUsedApplications(items);
981}
982
983void VFolderMenu::processMenu(QDomElement &docElem, int pass)
984{
985 SubMenu *parentMenu = m_currentMenu;
986 int oldDirectoryDirsCount = m_directoryDirs.count();
987
988 QString name;
989 QString directoryFile;
990 bool onlyUnallocated = false;
991 bool isDeleted = false;
992 QDomElement defaultLayoutNode;
993 QDomElement layoutNode;
994
995 QDomElement query;
996 QDomNode n = docElem.firstChild();
997 while (!n.isNull()) {
998 QDomElement e = n.toElement(); // try to convert the node to an element.
999 if (e.tagName() == QLatin1String("Name")) {
1000 name = e.text();
1001 } else if (e.tagName() == QLatin1String("Directory")) {
1002 directoryFile = e.text();
1003 } else if (e.tagName() == QLatin1String("DirectoryDir")) {
1004 QString dir = absoluteDir(dir: e.text(), baseDir: e.attribute(QStringLiteral("__BaseDir")));
1005
1006 m_directoryDirs.prepend(t: dir);
1007 } else if (e.tagName() == QLatin1String("OnlyUnallocated")) {
1008 onlyUnallocated = true;
1009 } else if (e.tagName() == QLatin1String("NotOnlyUnallocated")) {
1010 onlyUnallocated = false;
1011 } else if (e.tagName() == QLatin1String("Deleted")) {
1012 isDeleted = true;
1013 } else if (e.tagName() == QLatin1String("NotDeleted")) {
1014 isDeleted = false;
1015 } else if (e.tagName() == QLatin1String("DefaultLayout")) {
1016 defaultLayoutNode = e;
1017 } else if (e.tagName() == QLatin1String("Layout")) {
1018 layoutNode = e;
1019 }
1020 n = n.nextSibling();
1021 }
1022
1023 // Setup current menu entry
1024 if (pass == 0) {
1025 m_currentMenu = nullptr;
1026 // Look up menu
1027 if (parentMenu) {
1028 for (SubMenu *menu : std::as_const(t&: parentMenu->subMenus)) {
1029 if (menu->name == name) {
1030 m_currentMenu = menu;
1031 break;
1032 }
1033 }
1034 }
1035
1036 if (!m_currentMenu) { // Not found?
1037 // Create menu
1038 m_currentMenu = new SubMenu;
1039 m_currentMenu->name = name;
1040
1041 if (parentMenu) {
1042 parentMenu->subMenus.append(t: m_currentMenu);
1043 } else {
1044 m_rootMenu = m_currentMenu;
1045 }
1046 }
1047 if (directoryFile.isEmpty()) {
1048 // qCDebug(SYCOCA) << "Menu" << name << "does not specify a directory file.";
1049 }
1050
1051 // Override previous directoryFile iff available
1052 QString tmp = locateDirectoryFile(fileName: directoryFile);
1053 if (!tmp.isEmpty()) {
1054 m_currentMenu->directoryFile = tmp;
1055 }
1056 m_currentMenu->isDeleted = isDeleted;
1057
1058 m_currentMenu->defaultLayoutNode = defaultLayoutNode;
1059 m_currentMenu->layoutNode = layoutNode;
1060 } else {
1061 // Look up menu
1062 if (parentMenu) {
1063 for (SubMenu *menu : std::as_const(t&: parentMenu->subMenus)) {
1064 if (menu->name == name) {
1065 m_currentMenu = menu;
1066 break;
1067 }
1068 }
1069 } else {
1070 m_currentMenu = m_rootMenu;
1071 }
1072 }
1073
1074 // Process AppDir and LegacyDir
1075 if (pass == 0) {
1076 QDomElement query;
1077 QDomNode n = docElem.firstChild();
1078 while (!n.isNull()) {
1079 QDomElement e = n.toElement(); // try to convert the node to an element.
1080 if (e.tagName() == QLatin1String("AppDir")) {
1081 createAppsInfo();
1082 QString dir = absoluteDir(dir: e.text(), baseDir: e.attribute(QStringLiteral("__BaseDir")));
1083
1084 registerDirectory(directory: dir);
1085
1086 loadApplications(dir, prefix: QString());
1087 } else if (e.tagName() == QLatin1String("LegacyDir")) {
1088 createAppsInfo();
1089 QString dir = absoluteDir(dir: e.text(), baseDir: e.attribute(QStringLiteral("__BaseDir")));
1090
1091 QString prefix = e.attributes().namedItem(QStringLiteral("prefix")).toAttr().value();
1092
1093 SubMenu *oldMenu = m_currentMenu;
1094 m_currentMenu = new SubMenu;
1095
1096 registerDirectory(directory: dir);
1097
1098 processLegacyDir(dir, relDir: QString(), prefix);
1099
1100 m_legacyNodes.insert(key: dir, value: m_currentMenu);
1101 m_currentMenu = oldMenu;
1102 }
1103 n = n.nextSibling();
1104 }
1105 }
1106
1107 loadAppsInfo(); // Update the scope wrt the list of applications
1108
1109 if (((pass == 1) && !onlyUnallocated) || ((pass == 2) && onlyUnallocated)) {
1110 n = docElem.firstChild();
1111
1112 while (!n.isNull()) {
1113 QDomElement e = n.toElement(); // try to convert the node to an element.
1114 if (e.tagName() == QLatin1String("Include")) {
1115 QHash<QString, KService::Ptr> items;
1116
1117 QDomNode n2 = e.firstChild();
1118 while (!n2.isNull()) {
1119 QDomElement e2 = n2.toElement();
1120 items.clear();
1121 processCondition(domElem&: e2, items);
1122 if (m_track) {
1123 track(menuId: m_trackId, menuName: m_currentMenu->name, includeList: m_currentMenu->items, excludeList: m_currentMenu->excludeItems, itemList: items, QStringLiteral("Before <Include>"));
1124 }
1125 includeItems(items1&: m_currentMenu->items, items2: items);
1126 excludeItems(items1&: m_currentMenu->excludeItems, items2: items);
1127 markUsedApplications(items);
1128
1129 if (m_track) {
1130 track(menuId: m_trackId, menuName: m_currentMenu->name, includeList: m_currentMenu->items, excludeList: m_currentMenu->excludeItems, itemList: items, QStringLiteral("After <Include>"));
1131 }
1132
1133 n2 = n2.nextSibling();
1134 }
1135 }
1136
1137 else if (e.tagName() == QLatin1String("Exclude")) {
1138 QHash<QString, KService::Ptr> items;
1139
1140 QDomNode n2 = e.firstChild();
1141 while (!n2.isNull()) {
1142 QDomElement e2 = n2.toElement();
1143 items.clear();
1144 processCondition(domElem&: e2, items);
1145 if (m_track) {
1146 track(menuId: m_trackId, menuName: m_currentMenu->name, includeList: m_currentMenu->items, excludeList: m_currentMenu->excludeItems, itemList: items, QStringLiteral("Before <Exclude>"));
1147 }
1148 excludeItems(items1&: m_currentMenu->items, items2: items);
1149 includeItems(items1&: m_currentMenu->excludeItems, items2: items);
1150 if (m_track) {
1151 track(menuId: m_trackId, menuName: m_currentMenu->name, includeList: m_currentMenu->items, excludeList: m_currentMenu->excludeItems, itemList: items, QStringLiteral("After <Exclude>"));
1152 }
1153 n2 = n2.nextSibling();
1154 }
1155 }
1156
1157 n = n.nextSibling();
1158 }
1159 }
1160
1161 n = docElem.firstChild();
1162 while (!n.isNull()) {
1163 QDomElement e = n.toElement(); // try to convert the node to an element.
1164 if (e.tagName() == QLatin1String("Menu")) {
1165 processMenu(docElem&: e, pass);
1166 }
1167 // We insert legacy dir in pass 0, this way the order in the .menu-file determines
1168 // which .directory file gets used, but the menu-entries of legacy-menus will always
1169 // have the lowest priority.
1170 // else if (((pass == 1) && !onlyUnallocated) || ((pass == 2) && onlyUnallocated))
1171 else if (pass == 0) {
1172 if (e.tagName() == QLatin1String("LegacyDir")) {
1173 // Add legacy nodes to Menu structure
1174 QString dir = absoluteDir(dir: e.text(), baseDir: e.attribute(QStringLiteral("__BaseDir")));
1175 SubMenu *legacyMenu = m_legacyNodes[dir];
1176 if (legacyMenu) {
1177 mergeMenu(menu1: m_currentMenu, menu2: legacyMenu);
1178 }
1179 }
1180 }
1181 n = n.nextSibling();
1182 }
1183
1184 if (pass == 2) {
1185 n = docElem.firstChild();
1186 while (!n.isNull()) {
1187 QDomElement e = n.toElement(); // try to convert the node to an element.
1188 if (e.tagName() == QLatin1String("Move")) {
1189 QString orig;
1190 QString dest;
1191 QDomNode n2 = e.firstChild();
1192 while (!n2.isNull()) {
1193 QDomElement e2 = n2.toElement(); // try to convert the node to an element.
1194 if (e2.tagName() == QLatin1String("Old")) {
1195 orig = e2.text();
1196 }
1197 if (e2.tagName() == QLatin1String("New")) {
1198 dest = e2.text();
1199 }
1200 n2 = n2.nextSibling();
1201 }
1202 // qCDebug(SYCOCA) << "Moving" << orig << "to" << dest;
1203 if (!orig.isEmpty() && !dest.isEmpty()) {
1204 SubMenu *menu = takeSubMenu(parentMenu: m_currentMenu, menuName: orig);
1205 if (menu) {
1206 insertSubMenu(parentMenu: m_currentMenu, menuName: dest, newMenu: menu, reversePriority: true); // Destination has priority
1207 }
1208 }
1209 }
1210 n = n.nextSibling();
1211 }
1212 }
1213
1214 unloadAppsInfo(); // Update the scope wrt the list of applications
1215
1216 while (m_directoryDirs.count() > oldDirectoryDirsCount) {
1217 m_directoryDirs.pop_front();
1218 }
1219
1220 m_currentMenu = parentMenu;
1221}
1222
1223static QString parseAttribute(const QDomElement &e)
1224{
1225 QString option;
1226
1227 const QString SHOW_EMPTY = QStringLiteral("show_empty");
1228 if (e.hasAttribute(name: SHOW_EMPTY)) {
1229 QString str = e.attribute(name: SHOW_EMPTY);
1230 if (str == QLatin1String("true")) {
1231 option = QStringLiteral("ME ");
1232 } else if (str == QLatin1String("false")) {
1233 option = QStringLiteral("NME ");
1234 } else {
1235 // qCDebug(SYCOCA)<<" Error in parsing show_empty attribute :"<<str;
1236 }
1237 }
1238 const QString INLINE = QStringLiteral("inline");
1239 if (e.hasAttribute(name: INLINE)) {
1240 QString str = e.attribute(name: INLINE);
1241 if (str == QLatin1String("true")) {
1242 option += QLatin1String("I ");
1243 } else if (str == QLatin1String("false")) {
1244 option += QLatin1String("NI ");
1245 } else {
1246 qCDebug(SYCOCA) << " Error in parsing inline attribute :" << str;
1247 }
1248 }
1249 if (e.hasAttribute(QStringLiteral("inline_limit"))) {
1250 bool ok;
1251 int value = e.attribute(QStringLiteral("inline_limit")).toInt(ok: &ok);
1252 if (ok) {
1253 option += QStringLiteral("IL[%1] ").arg(a: value);
1254 }
1255 }
1256 if (e.hasAttribute(QStringLiteral("inline_header"))) {
1257 QString str = e.attribute(QStringLiteral("inline_header"));
1258 if (str == QLatin1String("true")) {
1259 option += QLatin1String("IH ");
1260 } else if (str == QLatin1String("false")) {
1261 option += QLatin1String("NIH ");
1262 } else {
1263 qCDebug(SYCOCA) << " Error in parsing of inline_header attribute :" << str;
1264 }
1265 }
1266 if (e.hasAttribute(QStringLiteral("inline_alias")) && e.attribute(QStringLiteral("inline_alias")) == QLatin1String("true")) {
1267 QString str = e.attribute(QStringLiteral("inline_alias"));
1268 if (str == QLatin1String("true")) {
1269 option += QLatin1String("IA");
1270 } else if (str == QLatin1String("false")) {
1271 option += QLatin1String("NIA");
1272 } else {
1273 qCDebug(SYCOCA) << " Error in parsing inline_alias attribute :" << str;
1274 }
1275 }
1276 if (!option.isEmpty()) {
1277 option.prepend(QStringLiteral(":O"));
1278 }
1279 return option;
1280}
1281
1282QStringList VFolderMenu::parseLayoutNode(const QDomElement &docElem) const
1283{
1284 QStringList layout;
1285
1286 QString optionDefaultLayout;
1287 if (docElem.tagName() == QLatin1String("DefaultLayout")) {
1288 optionDefaultLayout = parseAttribute(e: docElem);
1289 }
1290 if (!optionDefaultLayout.isEmpty()) {
1291 layout.append(t: optionDefaultLayout);
1292 }
1293
1294 bool mergeTagExists = false;
1295 QDomNode n = docElem.firstChild();
1296 while (!n.isNull()) {
1297 QDomElement e = n.toElement(); // try to convert the node to an element.
1298 if (e.tagName() == QLatin1String("Separator")) {
1299 layout.append(QStringLiteral(":S"));
1300 } else if (e.tagName() == QLatin1String("Filename")) {
1301 layout.append(t: e.text());
1302 } else if (e.tagName() == QLatin1String("Menuname")) {
1303 layout.append(t: QLatin1Char('/') + e.text());
1304 QString option = parseAttribute(e);
1305 if (!option.isEmpty()) {
1306 layout.append(t: option);
1307 }
1308 } else if (e.tagName() == QLatin1String("Merge")) {
1309 QString type = e.attributeNode(QStringLiteral("type")).value();
1310 if (type == QLatin1String("files")) {
1311 layout.append(QStringLiteral(":F"));
1312 } else if (type == QLatin1String("menus")) {
1313 layout.append(QStringLiteral(":M"));
1314 } else if (type == QLatin1String("all")) {
1315 layout.append(QStringLiteral(":A"));
1316 }
1317 mergeTagExists = true;
1318 }
1319
1320 n = n.nextSibling();
1321 }
1322
1323 if (!mergeTagExists) {
1324 layout.append(QStringLiteral(":M"));
1325 layout.append(QStringLiteral(":F"));
1326 qCWarning(SYCOCA) << "The menu spec file (" << m_docInfo.path
1327 << ") contains a Layout or DefaultLayout tag without the mandatory Merge tag inside. Please fix it.";
1328 }
1329 return layout;
1330}
1331
1332void VFolderMenu::layoutMenu(VFolderMenu::SubMenu *menu, QStringList defaultLayout) // krazy:exclude=passbyvalue
1333{
1334 if (!menu->defaultLayoutNode.isNull()) {
1335 defaultLayout = parseLayoutNode(docElem: menu->defaultLayoutNode);
1336 }
1337
1338 if (menu->layoutNode.isNull()) {
1339 menu->layoutList = defaultLayout;
1340 } else {
1341 menu->layoutList = parseLayoutNode(docElem: menu->layoutNode);
1342 if (menu->layoutList.isEmpty()) {
1343 menu->layoutList = defaultLayout;
1344 }
1345 }
1346
1347 for (VFolderMenu::SubMenu *subMenu : std::as_const(t&: menu->subMenus)) {
1348 layoutMenu(menu: subMenu, defaultLayout);
1349 }
1350}
1351
1352void VFolderMenu::markUsedApplications(const QHash<QString, KService::Ptr> &items)
1353{
1354 for (const KService::Ptr &p : items) {
1355 m_usedAppsDict.insert(value: p->menuId());
1356 }
1357}
1358
1359VFolderMenu::SubMenu *VFolderMenu::parseMenu(const QString &file)
1360{
1361 m_appsInfo = nullptr;
1362
1363 const QStringList dirs = QStandardPaths::locateAll(type: QStandardPaths::GenericConfigLocation, QStringLiteral("menus"), options: QStandardPaths::LocateDirectory);
1364 for (QStringList::ConstIterator it = dirs.begin(); it != dirs.end(); ++it) {
1365 registerDirectory(directory: *it);
1366 }
1367
1368 loadMenu(fileName: file);
1369
1370 delete m_rootMenu;
1371 m_rootMenu = m_currentMenu = nullptr;
1372
1373 QDomElement docElem = m_doc.documentElement();
1374
1375 for (int pass = 0; pass <= 2; pass++) {
1376 // pass 0: load application desktop files
1377 // pass 1: the normal processing
1378 // pass 2: process <OnlyUnallocated> to put unused files into "Lost & Found".
1379 processMenu(docElem, pass);
1380
1381 switch (pass) {
1382 case 0:
1383 // Fill the dictCategories for each AppsInfo in m_appsInfoList,
1384 // in preparation for processMenu pass 1.
1385 buildApplicationIndex(unusedOnly: false);
1386 break;
1387 case 1:
1388 // Fill the dictCategories for each AppsInfo in m_appsInfoList,
1389 // with only the unused apps, in preparation for processMenu pass 2.
1390 buildApplicationIndex(unusedOnly: true /* unusedOnly */);
1391 break;
1392 case 2: {
1393 QStringList defaultLayout;
1394 defaultLayout << QStringLiteral(":M"); // Sub-Menus
1395 defaultLayout << QStringLiteral(":F"); // Individual entries
1396 layoutMenu(menu: m_rootMenu, defaultLayout);
1397 break;
1398 }
1399 default:
1400 break;
1401 }
1402 }
1403
1404 return m_rootMenu;
1405}
1406
1407void VFolderMenu::setTrackId(const QString &id)
1408{
1409 m_track = !id.isEmpty();
1410 m_trackId = id;
1411}
1412
1413#include "moc_vfolder_menu_p.cpp"
1414

source code of kservice/src/sycoca/vfolder_menu.cpp