add more spacing
[personal-kdebase.git] / workspace / kmenuedit / menufile.cpp
bloba4f47a8fdb492233e1191812843120be82710bca
1 /*
2 * Copyright (C) 2003 Waldo Bastian <bastian@kde.org>
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License version 2 as
6 * published by the Free Software Foundation.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License
14 * along with this program; if not, write to the Free Software
15 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 #include <QFile>
20 #include <QTextStream>
21 #include <QRegExp>
22 #include <QFileInfo>
24 #include <kdebug.h>
25 #include <kglobal.h>
26 #include <klocale.h>
27 #include <kstandarddirs.h>
29 #include "menufile.h"
32 #define MF_MENU "Menu"
33 #define MF_PUBLIC_ID "-//freedesktop//DTD Menu 1.0//EN"
34 #define MF_SYSTEM_ID "http://www.freedesktop.org/standards/menu-spec/1.0/menu.dtd"
35 #define MF_NAME "Name"
36 #define MF_INCLUDE "Include"
37 #define MF_EXCLUDE "Exclude"
38 #define MF_FILENAME "Filename"
39 #define MF_DELETED "Deleted"
40 #define MF_NOTDELETED "NotDeleted"
41 #define MF_MOVE "Move"
42 #define MF_OLD "Old"
43 #define MF_NEW "New"
44 #define MF_DIRECTORY "Directory"
45 #define MF_LAYOUT "Layout"
46 #define MF_MENUNAME "Menuname"
47 #define MF_SEPARATOR "Separator"
48 #define MF_MERGE "Merge"
50 MenuFile::MenuFile(const QString &file)
51 : m_fileName(file), m_bDirty(false)
53 load();
56 MenuFile::~MenuFile()
60 bool MenuFile::load()
62 if (m_fileName.isEmpty())
63 return false;
65 QFile file( m_fileName );
66 if (!file.open( QIODevice::ReadOnly ))
68 if ( file.exists() )
69 kWarning() << "Could not read " << m_fileName ;
70 create();
71 return false;
74 QString errorMsg;
75 int errorRow;
76 int errorCol;
77 if ( !m_doc.setContent( &file, &errorMsg, &errorRow, &errorCol ) ) {
78 kWarning() << "Parse error in " << m_fileName << ", line " << errorRow << ", col " << errorCol << ": " << errorMsg ;
79 file.close();
80 create();
81 return false;
83 file.close();
85 return true;
88 void MenuFile::create()
90 QDomImplementation impl;
91 QDomDocumentType docType = impl.createDocumentType( MF_MENU, MF_PUBLIC_ID, MF_SYSTEM_ID );
92 m_doc = impl.createDocument(QString(), MF_MENU, docType);
95 bool MenuFile::save()
97 QFile file( m_fileName );
99 if (!file.open( QIODevice::WriteOnly ))
101 kWarning() << "Could not write " << m_fileName ;
102 m_error = i18n("Could not write to %1", m_fileName);
103 return false;
105 QTextStream stream( &file );
106 stream.setCodec( "UTF-8" );
108 stream << m_doc.toString();
110 file.close();
112 if (file.error() != QFile::NoError)
114 kWarning() << "Could not close " << m_fileName ;
115 m_error = i18n("Could not write to %1", m_fileName);
116 return false;
119 m_bDirty = false;
121 return true;
124 QDomElement MenuFile::findMenu(QDomElement elem, const QString &menuName, bool create)
126 QString menuNodeName;
127 QString subMenuName;
128 int i = menuName.indexOf('/');
129 if (i >= 0)
131 menuNodeName = menuName.left(i);
132 subMenuName = menuName.mid(i+1);
134 else
136 menuNodeName = menuName;
138 if (i == 0)
139 return findMenu(elem, subMenuName, create);
141 if (menuNodeName.isEmpty())
142 return elem;
144 QDomNode n = elem.firstChild();
145 while( !n.isNull() )
147 QDomElement e = n.toElement(); // try to convert the node to an element.
148 if (e.tagName() == MF_MENU)
150 QString name;
152 QDomNode n2 = e.firstChild();
153 while ( !n2.isNull() )
155 QDomElement e2 = n2.toElement();
156 if (!e2.isNull() && e2.tagName() == MF_NAME)
158 name = e2.text();
159 break;
161 n2 = n2.nextSibling();
164 if (name == menuNodeName)
166 if (subMenuName.isEmpty())
167 return e;
168 else
169 return findMenu(e, subMenuName, create);
172 n = n.nextSibling();
175 if (!create)
176 return QDomElement();
178 // Create new node.
179 QDomElement newElem = m_doc.createElement(MF_MENU);
180 QDomElement newNameElem = m_doc.createElement(MF_NAME);
181 newNameElem.appendChild(m_doc.createTextNode(menuNodeName));
182 newElem.appendChild(newNameElem);
183 elem.appendChild(newElem);
185 if (subMenuName.isEmpty())
186 return newElem;
187 else
188 return findMenu(newElem, subMenuName, create);
191 static QString entryToDirId(const QString &path)
193 // See also KDesktopFile::locateLocal
194 QString local;
195 if (QFileInfo(path).isAbsolute())
197 // XDG Desktop menu items come with absolute paths, we need to
198 // extract their relative path and then build a local path.
199 local = KGlobal::dirs()->relativeLocation("xdgdata-dirs", path);
202 if (local.isEmpty() || local.startsWith('/'))
204 // What now? Use filename only and hope for the best.
205 local = path.mid(path.lastIndexOf('/')+1);
207 return local;
210 static void purgeIncludesExcludes(QDomElement elem, const QString &appId, QDomElement &excludeNode, QDomElement &includeNode)
212 // Remove any previous includes/excludes of appId
213 QDomNode n = elem.firstChild();
214 while( !n.isNull() )
216 QDomElement e = n.toElement(); // try to convert the node to an element.
217 bool bIncludeNode = (e.tagName() == MF_INCLUDE);
218 bool bExcludeNode = (e.tagName() == MF_EXCLUDE);
219 if (bIncludeNode)
220 includeNode = e;
221 if (bExcludeNode)
222 excludeNode = e;
223 if (bIncludeNode || bExcludeNode)
225 QDomNode n2 = e.firstChild();
226 while ( !n2.isNull() )
228 QDomNode next = n2.nextSibling();
229 QDomElement e2 = n2.toElement();
230 if (!e2.isNull() && e2.tagName() == MF_FILENAME)
232 if (e2.text() == appId)
234 e.removeChild(e2);
235 break;
238 n2 = next;
241 n = n.nextSibling();
245 static void purgeDeleted(QDomElement elem)
247 // Remove any previous includes/excludes of appId
248 QDomNode n = elem.firstChild();
249 while( !n.isNull() )
251 QDomNode next = n.nextSibling();
252 QDomElement e = n.toElement(); // try to convert the node to an element.
253 if ((e.tagName() == MF_DELETED) ||
254 (e.tagName() == MF_NOTDELETED))
256 elem.removeChild(e);
258 n = next;
262 static void purgeLayout(QDomElement elem)
264 // Remove any previous includes/excludes of appId
265 QDomNode n = elem.firstChild();
266 while( !n.isNull() )
268 QDomNode next = n.nextSibling();
269 QDomElement e = n.toElement(); // try to convert the node to an element.
270 if (e.tagName() == MF_LAYOUT)
272 elem.removeChild(e);
274 n = next;
278 void MenuFile::addEntry(const QString &menuName, const QString &menuId)
280 m_bDirty = true;
282 m_removedEntries.removeAll(menuId);
284 QDomElement elem = findMenu(m_doc.documentElement(), menuName, true);
286 QDomElement excludeNode;
287 QDomElement includeNode;
289 purgeIncludesExcludes(elem, menuId, excludeNode, includeNode);
291 if (includeNode.isNull())
293 includeNode = m_doc.createElement(MF_INCLUDE);
294 elem.appendChild(includeNode);
297 QDomElement fileNode = m_doc.createElement(MF_FILENAME);
298 fileNode.appendChild(m_doc.createTextNode(menuId));
299 includeNode.appendChild(fileNode);
302 void MenuFile::setLayout(const QString &menuName, const QStringList &layout)
304 m_bDirty = true;
306 QDomElement elem = findMenu(m_doc.documentElement(), menuName, true);
308 purgeLayout(elem);
310 QDomElement layoutNode = m_doc.createElement(MF_LAYOUT);
311 elem.appendChild(layoutNode);
313 for(QStringList::ConstIterator it = layout.constBegin();
314 it != layout.constEnd(); ++it)
316 QString li = *it;
317 if (li == ":S")
319 layoutNode.appendChild(m_doc.createElement(MF_SEPARATOR));
321 else if (li == ":M")
323 QDomElement mergeNode = m_doc.createElement(MF_MERGE);
324 mergeNode.setAttribute("type", "menus");
325 layoutNode.appendChild(mergeNode);
327 else if (li == ":F")
329 QDomElement mergeNode = m_doc.createElement(MF_MERGE);
330 mergeNode.setAttribute("type", "files");
331 layoutNode.appendChild(mergeNode);
333 else if (li == ":A")
335 QDomElement mergeNode = m_doc.createElement(MF_MERGE);
336 mergeNode.setAttribute("type", "all");
337 layoutNode.appendChild(mergeNode);
339 else if (li.endsWith('/'))
341 li.truncate(li.length()-1);
342 QDomElement menuNode = m_doc.createElement(MF_MENUNAME);
343 menuNode.appendChild(m_doc.createTextNode(li));
344 layoutNode.appendChild(menuNode);
346 else
348 QDomElement fileNode = m_doc.createElement(MF_FILENAME);
349 fileNode.appendChild(m_doc.createTextNode(li));
350 layoutNode.appendChild(fileNode);
356 void MenuFile::removeEntry(const QString &menuName, const QString &menuId)
358 m_bDirty = true;
359 m_removedEntries.append(menuId);
361 QDomElement elem = findMenu(m_doc.documentElement(), menuName, true);
363 QDomElement excludeNode;
364 QDomElement includeNode;
366 purgeIncludesExcludes(elem, menuId, excludeNode, includeNode);
368 if (excludeNode.isNull())
370 excludeNode = m_doc.createElement(MF_EXCLUDE);
371 elem.appendChild(excludeNode);
373 QDomElement fileNode = m_doc.createElement(MF_FILENAME);
374 fileNode.appendChild(m_doc.createTextNode(menuId));
375 excludeNode.appendChild(fileNode);
378 void MenuFile::addMenu(const QString &menuName, const QString &menuFile)
380 m_bDirty = true;
381 QDomElement elem = findMenu(m_doc.documentElement(), menuName, true);
383 QDomElement dirElem = m_doc.createElement(MF_DIRECTORY);
384 dirElem.appendChild(m_doc.createTextNode(entryToDirId(menuFile)));
385 elem.appendChild(dirElem);
388 void MenuFile::moveMenu(const QString &oldMenu, const QString &newMenu)
390 m_bDirty = true;
392 // Undelete the new menu
393 QDomElement elem = findMenu(m_doc.documentElement(), newMenu, true);
394 purgeDeleted(elem);
395 elem.appendChild(m_doc.createElement(MF_NOTDELETED));
397 // TODO: GET RID OF COMMON PART, IT BREAKS STUFF
398 // Find common part
399 QStringList oldMenuParts = oldMenu.split( '/');
400 QStringList newMenuParts = newMenu.split( '/');
401 QString commonMenuName;
402 int max = qMin(oldMenuParts.count(), newMenuParts.count());
403 int i = 0;
404 for(; i < max; i++)
406 if (oldMenuParts[i] != newMenuParts[i])
407 break;
408 commonMenuName += '/' + oldMenuParts[i];
410 QString oldMenuName;
411 for(int j = i; j < oldMenuParts.count(); j++)
413 if (i != j)
414 oldMenuName += '/';
415 oldMenuName += oldMenuParts[j];
417 QString newMenuName;
418 for(int j = i; j < newMenuParts.count(); j++)
420 if (i != j)
421 newMenuName += '/';
422 newMenuName += newMenuParts[j];
425 if (oldMenuName == newMenuName) return; // Can happen
427 elem = findMenu(m_doc.documentElement(), commonMenuName, true);
429 // Add instructions for moving
430 QDomElement moveNode = m_doc.createElement(MF_MOVE);
431 QDomElement node = m_doc.createElement(MF_OLD);
432 node.appendChild(m_doc.createTextNode(oldMenuName));
433 moveNode.appendChild(node);
434 node = m_doc.createElement(MF_NEW);
435 node.appendChild(m_doc.createTextNode(newMenuName));
436 moveNode.appendChild(node);
437 elem.appendChild(moveNode);
440 void MenuFile::removeMenu(const QString &menuName)
442 m_bDirty = true;
444 QDomElement elem = findMenu(m_doc.documentElement(), menuName, true);
446 purgeDeleted(elem);
447 elem.appendChild(m_doc.createElement(MF_DELETED));
451 * Returns a unique menu-name for a new menu under @p menuName
452 * inspired by @p newMenu
454 QString MenuFile::uniqueMenuName(const QString &menuName, const QString &newMenu, const QStringList & excludeList)
456 QDomElement elem = findMenu(m_doc.documentElement(), menuName, false);
458 QString result = newMenu;
459 if (result.endsWith('/'))
460 result.truncate(result.length()-1);
462 QRegExp r("(.*)(?=-\\d+)");
463 result = (r.indexIn(result) > -1) ? r.cap(1) : result;
465 int trunc = result.length(); // Position of trailing '/'
467 result.append("/");
469 for(int n = 1; ++n; )
471 if (findMenu(elem, result, false).isNull() && !excludeList.contains(result))
472 return result;
474 result.truncate(trunc);
475 result.append(QString("-%1/").arg(n));
477 return QString(); // Never reached
480 void MenuFile::performAction(const ActionAtom *atom)
482 switch(atom->action)
484 case ADD_ENTRY:
485 addEntry(atom->arg1, atom->arg2);
486 return;
487 case REMOVE_ENTRY:
488 removeEntry(atom->arg1, atom->arg2);
489 return;
490 case ADD_MENU:
491 addMenu(atom->arg1, atom->arg2);
492 return;
493 case REMOVE_MENU:
494 removeMenu(atom->arg1);
495 return;
496 case MOVE_MENU:
497 moveMenu(atom->arg1, atom->arg2);
498 return;
502 MenuFile::ActionAtom *MenuFile::pushAction(MenuFile::ActionType action, const QString &arg1, const QString &arg2)
504 ActionAtom *atom = new ActionAtom;
505 atom->action = action;
506 atom->arg1 = arg1;
507 atom->arg2 = arg2;
508 m_actionList.append(atom);
509 return atom;
512 void MenuFile::popAction(ActionAtom *atom)
514 if (m_actionList.last() != atom)
516 qWarning("MenuFile::popAction Error, action not last in list.");
517 return;
519 m_actionList.removeLast();
520 delete atom;
523 bool MenuFile::performAllActions()
525 Q_FOREACH(ActionAtom *atom, m_actionList) {
526 performAction( atom );
527 delete atom;
529 m_actionList.clear();
531 // Entries that have been removed from the menu are added to .hidden
532 // so that they don't re-appear in Lost & Found
533 QStringList removed = m_removedEntries;
534 m_removedEntries.clear();
535 for(QStringList::ConstIterator it = removed.constBegin();
536 it != removed.constEnd(); ++it)
538 addEntry("/.hidden/", *it);
541 m_removedEntries.clear();
543 if (!m_bDirty)
544 return true;
546 return save();
549 bool MenuFile::dirty()
551 return (m_actionList.count() != 0) || m_bDirty;
554 void MenuFile::restoreMenuSystem( const QString &filename)
556 m_error.clear();
558 m_fileName = filename;
559 m_doc.clear();
560 m_bDirty = false;
561 Q_FOREACH(ActionAtom *atom, m_actionList) {
562 delete atom;
564 m_actionList.clear();
566 m_removedEntries.clear();
567 create();