/* * Copyright (C) 2003 Waldo Bastian <bastian@kde.org> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include <qfile.h> #include <qtextstream.h> #include <qregexp.h> #include <kdebug.h> #include <kglobal.h> #include <klocale.h> #include <kstandarddirs.h> #include "menufile.h" #define MF_MENU "Menu" #define MF_PUBLIC_ID "-//freedesktop//DTD Menu 1.0//EN" #define MF_SYSTEM_ID "http://www.freedesktop.org/standards/menu-spec/1.0/menu.dtd" #define MF_NAME "Name" #define MF_INCLUDE "Include" #define MF_EXCLUDE "Exclude" #define MF_FILENAME "Filename" #define MF_DELETED "Deleted" #define MF_NOTDELETED "NotDeleted" #define MF_MOVE "Move" #define MF_OLD "Old" #define MF_NEW "New" #define MF_DIRECTORY "Directory" #define MF_LAYOUT "Layout" #define MF_MENUNAME "Menuname" #define MF_SEPARATOR "Separator" #define MF_MERGE "Merge" MenuFile::MenuFile(const QString &file) : m_fileName(file), m_bDirty(false) { load(); } MenuFile::~MenuFile() { } bool MenuFile::load() { if (m_fileName.isEmpty()) return false; QFile file( m_fileName ); if (!file.open( IO_ReadOnly )) { kdWarning() << "Could not read " << m_fileName << endl; create(); return false; } QString errorMsg; int errorRow; int errorCol; if ( !m_doc.setContent( &file, &errorMsg, &errorRow, &errorCol ) ) { kdWarning() << "Parse error in " << m_fileName << ", line " << errorRow << ", col " << errorCol << ": " << errorMsg << endl; file.close(); create(); return false; } file.close(); return true; } void MenuFile::create() { QDomImplementation impl; QDomDocumentType docType = impl.createDocumentType( MF_MENU, MF_PUBLIC_ID, MF_SYSTEM_ID ); m_doc = impl.createDocument(QString::null, MF_MENU, docType); } bool MenuFile::save() { QFile file( m_fileName ); if (!file.open( IO_WriteOnly )) { kdWarning() << "Could not write " << m_fileName << endl; m_error = i18n("Could not write to %1").arg(m_fileName); return false; } QTextStream stream( &file ); stream.setEncoding(QTextStream::UnicodeUTF8); stream << m_doc.toString(); file.close(); if (file.status() != IO_Ok) { kdWarning() << "Could not close " << m_fileName << endl; m_error = i18n("Could not write to %1").arg(m_fileName); return false; } m_bDirty = false; return true; } QDomElement MenuFile::findMenu(QDomElement elem, const QString &menuName, bool create) { QString menuNodeName; QString subMenuName; int i = menuName.find('/'); if (i >= 0) { menuNodeName = menuName.left(i); subMenuName = menuName.mid(i+1); } else { menuNodeName = menuName; } if (i == 0) return findMenu(elem, subMenuName, create); if (menuNodeName.isEmpty()) return elem; QDomNode n = elem.firstChild(); while( !n.isNull() ) { QDomElement e = n.toElement(); // try to convert the node to an element. if (e.tagName() == MF_MENU) { QString name; QDomNode n2 = e.firstChild(); while ( !n2.isNull() ) { QDomElement e2 = n2.toElement(); if (!e2.isNull() && e2.tagName() == MF_NAME) { name = e2.text(); break; } n2 = n2.nextSibling(); } if (name == menuNodeName) { if (subMenuName.isEmpty()) return e; else return findMenu(e, subMenuName, create); } } n = n.nextSibling(); } if (!create) return QDomElement(); // Create new node. QDomElement newElem = m_doc.createElement(MF_MENU); QDomElement newNameElem = m_doc.createElement(MF_NAME); newNameElem.appendChild(m_doc.createTextNode(menuNodeName)); newElem.appendChild(newNameElem); elem.appendChild(newElem); if (subMenuName.isEmpty()) return newElem; else return findMenu(newElem, subMenuName, create); } static QString entryToDirId(const QString &path) { // See also KDesktopFile::locateLocal QString local; if (path.startsWith("/")) { // XDG Desktop menu items come with absolute paths, we need to // extract their relative path and then build a local path. local = KGlobal::dirs()->relativeLocation("xdgdata-dirs", path); } if (local.isEmpty() || local.startsWith("/")) { // What now? Use filename only and hope for the best. local = path.mid(path.findRev('/')+1); } return local; } static void purgeIncludesExcludes(QDomElement elem, const QString &appId, QDomElement &excludeNode, QDomElement &includeNode) { // Remove any previous includes/excludes of appId QDomNode n = elem.firstChild(); while( !n.isNull() ) { QDomElement e = n.toElement(); // try to convert the node to an element. bool bIncludeNode = (e.tagName() == MF_INCLUDE); bool bExcludeNode = (e.tagName() == MF_EXCLUDE); if (bIncludeNode) includeNode = e; if (bExcludeNode) excludeNode = e; if (bIncludeNode || bExcludeNode) { QDomNode n2 = e.firstChild(); while ( !n2.isNull() ) { QDomNode next = n2.nextSibling(); QDomElement e2 = n2.toElement(); if (!e2.isNull() && e2.tagName() == MF_FILENAME) { if (e2.text() == appId) { e.removeChild(e2); break; } } n2 = next; } } n = n.nextSibling(); } } static void purgeDeleted(QDomElement elem) { // Remove any previous includes/excludes of appId QDomNode n = elem.firstChild(); while( !n.isNull() ) { QDomNode next = n.nextSibling(); QDomElement e = n.toElement(); // try to convert the node to an element. if ((e.tagName() == MF_DELETED) || (e.tagName() == MF_NOTDELETED)) { elem.removeChild(e); } n = next; } } static void purgeLayout(QDomElement elem) { // Remove any previous includes/excludes of appId QDomNode n = elem.firstChild(); while( !n.isNull() ) { QDomNode next = n.nextSibling(); QDomElement e = n.toElement(); // try to convert the node to an element. if (e.tagName() == MF_LAYOUT) { elem.removeChild(e); } n = next; } } void MenuFile::addEntry(const QString &menuName, const QString &menuId) { m_bDirty = true; m_removedEntries.remove(menuId); QDomElement elem = findMenu(m_doc.documentElement(), menuName, true); QDomElement excludeNode; QDomElement includeNode; purgeIncludesExcludes(elem, menuId, excludeNode, includeNode); if (includeNode.isNull()) { includeNode = m_doc.createElement(MF_INCLUDE); elem.appendChild(includeNode); } QDomElement fileNode = m_doc.createElement(MF_FILENAME); fileNode.appendChild(m_doc.createTextNode(menuId)); includeNode.appendChild(fileNode); } void MenuFile::setLayout(const QString &menuName, const QStringList &layout) { m_bDirty = true; QDomElement elem = findMenu(m_doc.documentElement(), menuName, true); purgeLayout(elem); QDomElement layoutNode = m_doc.createElement(MF_LAYOUT); elem.appendChild(layoutNode); for(QStringList::ConstIterator it = layout.begin(); it != layout.end(); ++it) { QString li = *it; if (li == ":S") { layoutNode.appendChild(m_doc.createElement(MF_SEPARATOR)); } else if (li == ":M") { QDomElement mergeNode = m_doc.createElement(MF_MERGE); mergeNode.setAttribute("type", "menus"); layoutNode.appendChild(mergeNode); } else if (li == ":F") { QDomElement mergeNode = m_doc.createElement(MF_MERGE); mergeNode.setAttribute("type", "files"); layoutNode.appendChild(mergeNode); } else if (li == ":A") { QDomElement mergeNode = m_doc.createElement(MF_MERGE); mergeNode.setAttribute("type", "all"); layoutNode.appendChild(mergeNode); } else if (li.endsWith("/")) { li.truncate(li.length()-1); QDomElement menuNode = m_doc.createElement(MF_MENUNAME); menuNode.appendChild(m_doc.createTextNode(li)); layoutNode.appendChild(menuNode); } else { QDomElement fileNode = m_doc.createElement(MF_FILENAME); fileNode.appendChild(m_doc.createTextNode(li)); layoutNode.appendChild(fileNode); } } } void MenuFile::removeEntry(const QString &menuName, const QString &menuId) { m_bDirty = true; m_removedEntries.append(menuId); QDomElement elem = findMenu(m_doc.documentElement(), menuName, true); QDomElement excludeNode; QDomElement includeNode; purgeIncludesExcludes(elem, menuId, excludeNode, includeNode); if (excludeNode.isNull()) { excludeNode = m_doc.createElement(MF_EXCLUDE); elem.appendChild(excludeNode); } QDomElement fileNode = m_doc.createElement(MF_FILENAME); fileNode.appendChild(m_doc.createTextNode(menuId)); excludeNode.appendChild(fileNode); } void MenuFile::addMenu(const QString &menuName, const QString &menuFile) { m_bDirty = true; QDomElement elem = findMenu(m_doc.documentElement(), menuName, true); QDomElement dirElem = m_doc.createElement(MF_DIRECTORY); dirElem.appendChild(m_doc.createTextNode(entryToDirId(menuFile))); elem.appendChild(dirElem); } void MenuFile::moveMenu(const QString &oldMenu, const QString &newMenu) { m_bDirty = true; // Undelete the new menu QDomElement elem = findMenu(m_doc.documentElement(), newMenu, true); purgeDeleted(elem); elem.appendChild(m_doc.createElement(MF_NOTDELETED)); // TODO: GET RID OF COMMON PART, IT BREAKS STUFF // Find common part QStringList oldMenuParts = QStringList::split('/', oldMenu); QStringList newMenuParts = QStringList::split('/', newMenu); QString commonMenuName; uint max = QMIN(oldMenuParts.count(), newMenuParts.count()); uint i = 0; for(; i < max; i++) { if (oldMenuParts[i] != newMenuParts[i]) break; commonMenuName += '/' + oldMenuParts[i]; } QString oldMenuName; for(uint j = i; j < oldMenuParts.count(); j++) { if (i != j) oldMenuName += '/'; oldMenuName += oldMenuParts[j]; } QString newMenuName; for(uint j = i; j < newMenuParts.count(); j++) { if (i != j) newMenuName += '/'; newMenuName += newMenuParts[j]; } if (oldMenuName == newMenuName) return; // Can happen elem = findMenu(m_doc.documentElement(), commonMenuName, true); // Add instructions for moving QDomElement moveNode = m_doc.createElement(MF_MOVE); QDomElement node = m_doc.createElement(MF_OLD); node.appendChild(m_doc.createTextNode(oldMenuName)); moveNode.appendChild(node); node = m_doc.createElement(MF_NEW); node.appendChild(m_doc.createTextNode(newMenuName)); moveNode.appendChild(node); elem.appendChild(moveNode); } void MenuFile::removeMenu(const QString &menuName) { m_bDirty = true; QDomElement elem = findMenu(m_doc.documentElement(), menuName, true); purgeDeleted(elem); elem.appendChild(m_doc.createElement(MF_DELETED)); } /** * Returns a unique menu-name for a new menu under @p menuName * inspired by @p newMenu */ QString MenuFile::uniqueMenuName(const QString &menuName, const QString &newMenu, const QStringList & excludeList) { QDomElement elem = findMenu(m_doc.documentElement(), menuName, false); QString result = newMenu; if (result.endsWith("/")) result.truncate(result.length()-1); QRegExp r("(.*)(?=-\\d+)"); result = (r.search(result) > -1) ? r.cap(1) : result; int trunc = result.length(); // Position of trailing '/' result.append("/"); for(int n = 1; ++n; ) { if (findMenu(elem, result, false).isNull() && !excludeList.contains(result)) return result; result.truncate(trunc); result.append(QString("-%1/").arg(n)); } return QString::null; // Never reached } void MenuFile::performAction(const ActionAtom *atom) { switch(atom->action) { case ADD_ENTRY: addEntry(atom->arg1, atom->arg2); return; case REMOVE_ENTRY: removeEntry(atom->arg1, atom->arg2); return; case ADD_MENU: addMenu(atom->arg1, atom->arg2); return; case REMOVE_MENU: removeMenu(atom->arg1); return; case MOVE_MENU: moveMenu(atom->arg1, atom->arg2); return; } } MenuFile::ActionAtom *MenuFile::pushAction(MenuFile::ActionType action, const QString &arg1, const QString &arg2) { ActionAtom *atom = new ActionAtom; atom->action = action; atom->arg1 = arg1; atom->arg2 = arg2; m_actionList.append(atom); return atom; } void MenuFile::popAction(ActionAtom *atom) { if (m_actionList.getLast() != atom) { qWarning("MenuFile::popAction Error, action not last in list."); return; } m_actionList.removeLast(); delete atom; } bool MenuFile::performAllActions() { for(ActionAtom *atom; (atom = m_actionList.getFirst()); m_actionList.removeFirst()) { performAction(atom); delete atom; } // Entries that have been removed from the menu are added to .hidden // so that they don't re-appear in Lost & Found QStringList removed = m_removedEntries; m_removedEntries.clear(); for(QStringList::ConstIterator it = removed.begin(); it != removed.end(); ++it) { addEntry("/.hidden/", *it); } m_removedEntries.clear(); if (!m_bDirty) return true; return save(); } bool MenuFile::dirty() { return (m_actionList.count() != 0) || m_bDirty; }