You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tdebase/kmenuedit/menufile.cpp

553 lines
14 KiB

/*
* 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;
}