File indexing completed on 2025-04-27 08:14:00
0001 /* 0002 * SPDX-FileCopyrightText: 2003 Waldo Bastian <bastian@kde.org> 0003 * 0004 * SPDX-License-Identifier: GPL-2.0-only 0005 * 0006 */ 0007 0008 #include "menufile.h" 0009 0010 #include <QDir> 0011 #include <QFile> 0012 #include <QFileInfo> 0013 #include <QRegularExpression> 0014 #include <QStandardPaths> 0015 #include <QTextStream> 0016 0017 #include "kmenuedit_debug.h" 0018 #include <KLocalizedString> 0019 0020 const QString MenuFile::MF_MENU = QStringLiteral("Menu"); 0021 const QString MenuFile::MF_PUBLIC_ID = QStringLiteral("-//freedesktop//DTD Menu 1.0//EN"); 0022 const QString MenuFile::MF_SYSTEM_ID = QStringLiteral("http://www.freedesktop.org/standards/menu-spec/1.0/menu.dtd"); 0023 const QString MenuFile::MF_NAME = QStringLiteral("Name"); 0024 const QString MenuFile::MF_INCLUDE = QStringLiteral("Include"); 0025 const QString MenuFile::MF_EXCLUDE = QStringLiteral("Exclude"); 0026 const QString MenuFile::MF_FILENAME = QStringLiteral("Filename"); 0027 const QString MenuFile::MF_DELETED = QStringLiteral("Deleted"); 0028 const QString MenuFile::MF_NOTDELETED = QStringLiteral("NotDeleted"); 0029 const QString MenuFile::MF_MOVE = QStringLiteral("Move"); 0030 const QString MenuFile::MF_OLD = QStringLiteral("Old"); 0031 const QString MenuFile::MF_NEW = QStringLiteral("New"); 0032 const QString MenuFile::MF_DIRECTORY = QStringLiteral("Directory"); 0033 const QString MenuFile::MF_LAYOUT = QStringLiteral("Layout"); 0034 const QString MenuFile::MF_MENUNAME = QStringLiteral("Menuname"); 0035 const QString MenuFile::MF_SEPARATOR = QStringLiteral("Separator"); 0036 const QString MenuFile::MF_MERGE = QStringLiteral("Merge"); 0037 0038 MenuFile::MenuFile(const QString &file) 0039 : m_fileName(file) 0040 , m_bDirty(false) 0041 { 0042 load(); 0043 } 0044 0045 MenuFile::~MenuFile() 0046 { 0047 } 0048 0049 bool MenuFile::load() 0050 { 0051 if (m_fileName.isEmpty()) { 0052 return false; 0053 } 0054 0055 QFile file(m_fileName); 0056 if (!file.open(QIODevice::ReadOnly)) { 0057 if (file.exists()) { 0058 qCWarning(KMENUEDIT_LOG) << "Could not read " << m_fileName; 0059 } 0060 create(); 0061 return false; 0062 } 0063 0064 QString errorMsg; 0065 int errorRow; 0066 int errorCol; 0067 if (!m_doc.setContent(&file, &errorMsg, &errorRow, &errorCol)) { 0068 qCWarning(KMENUEDIT_LOG) << "Parse error in " << m_fileName << ", line " << errorRow << ", col " << errorCol << ": " << errorMsg; 0069 file.close(); 0070 create(); 0071 return false; 0072 } 0073 file.close(); 0074 0075 return true; 0076 } 0077 0078 void MenuFile::create() 0079 { 0080 QDomImplementation impl; 0081 QDomDocumentType docType = impl.createDocumentType(MF_MENU, MF_PUBLIC_ID, MF_SYSTEM_ID); 0082 m_doc = impl.createDocument(QString(), MF_MENU, docType); 0083 } 0084 0085 bool MenuFile::save() 0086 { 0087 QFile file(m_fileName); 0088 // create directory if it doesn't exist 0089 QFileInfo info(file); 0090 info.dir().mkpath(QStringLiteral(".")); 0091 0092 if (!file.open(QIODevice::WriteOnly)) { 0093 qCWarning(KMENUEDIT_LOG) << "Could not write " << m_fileName; 0094 m_error = i18n("Could not write to %1", m_fileName); 0095 return false; 0096 } 0097 QTextStream stream(&file); 0098 0099 stream << m_doc.toString(); 0100 0101 file.close(); 0102 0103 if (file.error() != QFile::NoError) { 0104 qCWarning(KMENUEDIT_LOG) << "Could not close " << m_fileName; 0105 m_error = i18n("Could not write to %1", m_fileName); 0106 return false; 0107 } 0108 0109 m_bDirty = false; 0110 0111 return true; 0112 } 0113 0114 QDomElement MenuFile::findMenu(QDomElement elem, const QString &menuName, bool create) 0115 { 0116 QString menuNodeName; 0117 QString subMenuName; 0118 int i = menuName.indexOf(QLatin1Char('/')); 0119 if (i >= 0) { 0120 menuNodeName = menuName.left(i); 0121 subMenuName = menuName.mid(i + 1); 0122 } else { 0123 menuNodeName = menuName; 0124 } 0125 if (i == 0) { 0126 return findMenu(elem, subMenuName, create); 0127 } 0128 0129 if (menuNodeName.isEmpty()) { 0130 return elem; 0131 } 0132 0133 QDomNode n = elem.firstChild(); 0134 while (!n.isNull()) { 0135 QDomElement e = n.toElement(); // try to convert the node to an element. 0136 if (e.tagName() == MF_MENU) { 0137 QString name; 0138 0139 QDomNode n2 = e.firstChild(); 0140 while (!n2.isNull()) { 0141 QDomElement e2 = n2.toElement(); 0142 if (!e2.isNull() && e2.tagName() == MF_NAME) { 0143 name = e2.text(); 0144 break; 0145 } 0146 n2 = n2.nextSibling(); 0147 } 0148 0149 if (name == menuNodeName) { 0150 if (subMenuName.isEmpty()) { 0151 return e; 0152 } else { 0153 return findMenu(e, subMenuName, create); 0154 } 0155 } 0156 } 0157 n = n.nextSibling(); 0158 } 0159 0160 if (!create) { 0161 return QDomElement(); 0162 } 0163 0164 // Create new node. 0165 QDomElement newElem = m_doc.createElement(MF_MENU); 0166 QDomElement newNameElem = m_doc.createElement(MF_NAME); 0167 newNameElem.appendChild(m_doc.createTextNode(menuNodeName)); 0168 newElem.appendChild(newNameElem); 0169 elem.appendChild(newElem); 0170 0171 if (subMenuName.isEmpty()) { 0172 return newElem; 0173 } else { 0174 return findMenu(newElem, subMenuName, create); 0175 } 0176 } 0177 0178 static QString relativeToDesktopDirsLocation(const QString &file) 0179 { 0180 const QString canonical = QFileInfo(file).canonicalFilePath(); 0181 const QStringList dirs = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation); 0182 for (const QString &dir : dirs) { 0183 const QString base = dir + QStringLiteral("/desktop-directories"); 0184 if (canonical.startsWith(base)) { 0185 return canonical.mid(base.length() + 1); 0186 } 0187 } 0188 return QString(); 0189 } 0190 0191 static QString entryToDirId(const QString &path) 0192 { 0193 // See also KDesktopFile::locateLocal 0194 QString local; 0195 if (QFileInfo(path).isAbsolute()) { 0196 // XDG Desktop menu items come with absolute paths, we need to 0197 // extract their relative path and then build a local path. 0198 local = relativeToDesktopDirsLocation(path); 0199 } 0200 0201 if (local.isEmpty() || local.startsWith(QLatin1Char('/'))) { 0202 // What now? Use filename only and hope for the best. 0203 local = path.mid(path.lastIndexOf(QLatin1Char('/')) + 1); 0204 } 0205 return local; 0206 } 0207 0208 static void purgeIncludesExcludes(const QDomElement &elem, const QString &appId, QDomElement &excludeNode, QDomElement &includeNode) 0209 { 0210 // Remove any previous includes/excludes of appId 0211 QDomNode n = elem.firstChild(); 0212 while (!n.isNull()) { 0213 QDomElement e = n.toElement(); // try to convert the node to an element. 0214 bool bIncludeNode = (e.tagName() == MenuFile::MF_INCLUDE); 0215 bool bExcludeNode = (e.tagName() == MenuFile::MF_EXCLUDE); 0216 if (bIncludeNode) { 0217 includeNode = e; 0218 } 0219 if (bExcludeNode) { 0220 excludeNode = e; 0221 } 0222 if (bIncludeNode || bExcludeNode) { 0223 QDomNode n2 = e.firstChild(); 0224 while (!n2.isNull()) { 0225 QDomNode next = n2.nextSibling(); 0226 QDomElement e2 = n2.toElement(); 0227 if (!e2.isNull() && e2.tagName() == MenuFile::MF_FILENAME) { 0228 if (e2.text() == appId) { 0229 e.removeChild(e2); 0230 break; 0231 } 0232 } 0233 n2 = next; 0234 } 0235 } 0236 n = n.nextSibling(); 0237 } 0238 } 0239 0240 static void purgeDeleted(QDomElement elem) 0241 { 0242 // Remove any previous includes/excludes of appId 0243 QDomNode n = elem.firstChild(); 0244 while (!n.isNull()) { 0245 QDomNode next = n.nextSibling(); 0246 QDomElement e = n.toElement(); // try to convert the node to an element. 0247 if ((e.tagName() == MenuFile::MF_DELETED) || (e.tagName() == MenuFile::MF_NOTDELETED)) { 0248 elem.removeChild(e); 0249 } 0250 n = next; 0251 } 0252 } 0253 0254 static void purgeLayout(QDomElement elem) 0255 { 0256 // Remove any previous includes/excludes of appId 0257 QDomNode n = elem.firstChild(); 0258 while (!n.isNull()) { 0259 QDomNode next = n.nextSibling(); 0260 QDomElement e = n.toElement(); // try to convert the node to an element. 0261 if (e.tagName() == MenuFile::MF_LAYOUT) { 0262 elem.removeChild(e); 0263 } 0264 n = next; 0265 } 0266 } 0267 0268 void MenuFile::addEntry(const QString &menuName, const QString &menuId) 0269 { 0270 m_bDirty = true; 0271 0272 m_removedEntries.removeAll(menuId); 0273 0274 QDomElement elem = findMenu(m_doc.documentElement(), menuName, true); 0275 0276 QDomElement excludeNode; 0277 QDomElement includeNode; 0278 0279 purgeIncludesExcludes(elem, menuId, excludeNode, includeNode); 0280 0281 if (includeNode.isNull()) { 0282 includeNode = m_doc.createElement(MF_INCLUDE); 0283 elem.appendChild(includeNode); 0284 } 0285 0286 QDomElement fileNode = m_doc.createElement(MF_FILENAME); 0287 fileNode.appendChild(m_doc.createTextNode(menuId)); 0288 includeNode.appendChild(fileNode); 0289 } 0290 0291 void MenuFile::setLayout(const QString &menuName, const QStringList &layout) 0292 { 0293 m_bDirty = true; 0294 0295 QDomElement elem = findMenu(m_doc.documentElement(), menuName, true); 0296 0297 purgeLayout(elem); 0298 0299 QDomElement layoutNode = m_doc.createElement(MF_LAYOUT); 0300 elem.appendChild(layoutNode); 0301 0302 for (QStringList::ConstIterator it = layout.constBegin(); it != layout.constEnd(); ++it) { 0303 QString li = *it; 0304 if (li == QLatin1String(":S")) { 0305 layoutNode.appendChild(m_doc.createElement(MF_SEPARATOR)); 0306 } else if (li == QLatin1String(":M")) { 0307 QDomElement mergeNode = m_doc.createElement(MF_MERGE); 0308 mergeNode.setAttribute(QStringLiteral("type"), QStringLiteral("menus")); 0309 layoutNode.appendChild(mergeNode); 0310 } else if (li == QLatin1String(":F")) { 0311 QDomElement mergeNode = m_doc.createElement(MF_MERGE); 0312 mergeNode.setAttribute(QStringLiteral("type"), QStringLiteral("files")); 0313 layoutNode.appendChild(mergeNode); 0314 } else if (li == QLatin1String(":A")) { 0315 QDomElement mergeNode = m_doc.createElement(MF_MERGE); 0316 mergeNode.setAttribute(QStringLiteral("type"), QStringLiteral("all")); 0317 layoutNode.appendChild(mergeNode); 0318 } else if (li.endsWith(QLatin1Char('/'))) { 0319 li.chop(1); 0320 QDomElement menuNode = m_doc.createElement(MF_MENUNAME); 0321 menuNode.appendChild(m_doc.createTextNode(li)); 0322 layoutNode.appendChild(menuNode); 0323 } else { 0324 QDomElement fileNode = m_doc.createElement(MF_FILENAME); 0325 fileNode.appendChild(m_doc.createTextNode(li)); 0326 layoutNode.appendChild(fileNode); 0327 } 0328 } 0329 } 0330 0331 void MenuFile::removeEntry(const QString &menuName, const QString &menuId) 0332 { 0333 m_bDirty = true; 0334 m_removedEntries.append(menuId); 0335 0336 QDomElement elem = findMenu(m_doc.documentElement(), menuName, true); 0337 0338 QDomElement excludeNode; 0339 QDomElement includeNode; 0340 0341 purgeIncludesExcludes(elem, menuId, excludeNode, includeNode); 0342 0343 if (excludeNode.isNull()) { 0344 excludeNode = m_doc.createElement(MF_EXCLUDE); 0345 elem.appendChild(excludeNode); 0346 } 0347 QDomElement fileNode = m_doc.createElement(MF_FILENAME); 0348 fileNode.appendChild(m_doc.createTextNode(menuId)); 0349 excludeNode.appendChild(fileNode); 0350 } 0351 0352 void MenuFile::addMenu(const QString &menuName, const QString &menuFile) 0353 { 0354 m_bDirty = true; 0355 QDomElement elem = findMenu(m_doc.documentElement(), menuName, true); 0356 0357 QDomElement dirElem = m_doc.createElement(MF_DIRECTORY); 0358 dirElem.appendChild(m_doc.createTextNode(entryToDirId(menuFile))); 0359 elem.appendChild(dirElem); 0360 } 0361 0362 void MenuFile::moveMenu(const QString &oldMenu, const QString &newMenu) 0363 { 0364 m_bDirty = true; 0365 0366 // Undelete the new menu 0367 QDomElement elem = findMenu(m_doc.documentElement(), newMenu, true); 0368 purgeDeleted(elem); 0369 elem.appendChild(m_doc.createElement(MF_NOTDELETED)); 0370 0371 // TODO: GET RID OF COMMON PART, IT BREAKS STUFF 0372 // Find common part 0373 QStringList oldMenuParts = oldMenu.split(QLatin1Char('/')); 0374 QStringList newMenuParts = newMenu.split(QLatin1Char('/')); 0375 QString commonMenuName; 0376 int max = qMin(oldMenuParts.count(), newMenuParts.count()); 0377 int i = 0; 0378 for (; i < max; i++) { 0379 if (oldMenuParts[i] != newMenuParts[i]) { 0380 break; 0381 } 0382 commonMenuName += QLatin1Char('/') + oldMenuParts[i]; 0383 } 0384 QString oldMenuName; 0385 for (int j = i; j < oldMenuParts.count() - 1; j++) { 0386 if (i != j) { 0387 oldMenuName += QLatin1Char('/'); 0388 } 0389 oldMenuName += oldMenuParts[j]; 0390 } 0391 QString newMenuName; 0392 for (int j = i; j < newMenuParts.count() - 1; j++) { 0393 if (i != j) { 0394 newMenuName += QLatin1Char('/'); 0395 } 0396 newMenuName += newMenuParts[j]; 0397 } 0398 0399 if (oldMenuName == newMenuName) { 0400 return; // Can happen 0401 } 0402 elem = findMenu(m_doc.documentElement(), commonMenuName, true); 0403 0404 // Add instructions for moving 0405 QDomElement moveNode = m_doc.createElement(MF_MOVE); 0406 QDomElement node = m_doc.createElement(MF_OLD); 0407 node.appendChild(m_doc.createTextNode(oldMenuName)); 0408 moveNode.appendChild(node); 0409 node = m_doc.createElement(MF_NEW); 0410 node.appendChild(m_doc.createTextNode(newMenuName)); 0411 moveNode.appendChild(node); 0412 elem.appendChild(moveNode); 0413 } 0414 0415 void MenuFile::removeMenu(const QString &menuName) 0416 { 0417 m_bDirty = true; 0418 0419 QDomElement elem = findMenu(m_doc.documentElement(), menuName, true); 0420 0421 purgeDeleted(elem); 0422 elem.appendChild(m_doc.createElement(MF_DELETED)); 0423 } 0424 0425 /** 0426 * Returns a unique menu-name for a new menu under @p menuName 0427 * inspired by @p newMenu 0428 */ 0429 QString MenuFile::uniqueMenuName(const QString &menuName, const QString &newMenu, const QStringList &excludeList) 0430 { 0431 QDomElement elem = findMenu(m_doc.documentElement(), menuName, false); 0432 0433 QString result = newMenu; 0434 if (result.endsWith(QLatin1Char('/'))) { 0435 result.chop(1); 0436 } 0437 0438 static const QRegularExpression re(QStringLiteral("(.*)(?=-\\d+)")); 0439 const QRegularExpressionMatch match = re.match(result); 0440 result = match.hasMatch() ? match.captured(1) : result; 0441 0442 int trunc = result.length(); // Position of trailing '/' 0443 0444 result.append(QLatin1Char('/')); 0445 0446 for (int n = 1; ++n;) { 0447 if (findMenu(elem, result, false).isNull() && !excludeList.contains(result)) { 0448 return result; 0449 } 0450 0451 result.truncate(trunc); 0452 result.append(QStringLiteral("-%1/").arg(n)); 0453 } 0454 return QString(); // Never reached 0455 } 0456 0457 void MenuFile::performAction(const ActionAtom *atom) 0458 { 0459 switch (atom->action) { 0460 case ADD_ENTRY: 0461 addEntry(atom->arg1, atom->arg2); 0462 return; 0463 case REMOVE_ENTRY: 0464 removeEntry(atom->arg1, atom->arg2); 0465 return; 0466 case ADD_MENU: 0467 addMenu(atom->arg1, atom->arg2); 0468 return; 0469 case REMOVE_MENU: 0470 removeMenu(atom->arg1); 0471 return; 0472 case MOVE_MENU: 0473 moveMenu(atom->arg1, atom->arg2); 0474 return; 0475 } 0476 } 0477 0478 MenuFile::ActionAtom *MenuFile::pushAction(MenuFile::ActionType action, const QString &arg1, const QString &arg2) 0479 { 0480 ActionAtom *atom = new ActionAtom; 0481 atom->action = action; 0482 atom->arg1 = arg1; 0483 atom->arg2 = arg2; 0484 m_actionList.append(atom); 0485 return atom; 0486 } 0487 0488 void MenuFile::popAction(ActionAtom *atom) 0489 { 0490 if (m_actionList.last() != atom) { 0491 qWarning("MenuFile::popAction Error, action not last in list."); 0492 return; 0493 } 0494 m_actionList.removeLast(); 0495 delete atom; 0496 } 0497 0498 bool MenuFile::performAllActions() 0499 { 0500 Q_FOREACH (ActionAtom *atom, m_actionList) { 0501 performAction(atom); 0502 delete atom; 0503 } 0504 m_actionList.clear(); 0505 0506 // Entries that have been removed from the menu are added to .hidden 0507 // so that they don't re-appear in Lost & Found 0508 QStringList removed = m_removedEntries; 0509 m_removedEntries.clear(); 0510 for (QStringList::ConstIterator it = removed.constBegin(); it != removed.constEnd(); ++it) { 0511 addEntry(QStringLiteral("/.hidden/"), *it); 0512 } 0513 0514 m_removedEntries.clear(); 0515 0516 if (!m_bDirty) { 0517 return true; 0518 } 0519 0520 return save(); 0521 } 0522 0523 bool MenuFile::dirty() const 0524 { 0525 return (m_actionList.count() != 0) || m_bDirty; 0526 } 0527 0528 void MenuFile::restoreMenuSystem(const QString &filename) 0529 { 0530 m_error.clear(); 0531 0532 m_fileName = filename; 0533 m_doc.clear(); 0534 m_bDirty = false; 0535 Q_FOREACH (ActionAtom *atom, m_actionList) { 0536 delete atom; 0537 } 0538 m_actionList.clear(); 0539 0540 m_removedEntries.clear(); 0541 create(); 0542 }