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.
20 #include <QTextStream>
27 #include <kstandarddirs.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"
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)
62 if (m_fileName
.isEmpty())
65 QFile
file( m_fileName
);
66 if (!file
.open( QIODevice::ReadOnly
))
69 kWarning() << "Could not read " << m_fileName
;
77 if ( !m_doc
.setContent( &file
, &errorMsg
, &errorRow
, &errorCol
) ) {
78 kWarning() << "Parse error in " << m_fileName
<< ", line " << errorRow
<< ", col " << errorCol
<< ": " << errorMsg
;
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
);
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
);
105 QTextStream
stream( &file
);
106 stream
.setCodec( "UTF-8" );
108 stream
<< m_doc
.toString();
112 if (file
.error() != QFile::NoError
)
114 kWarning() << "Could not close " << m_fileName
;
115 m_error
= i18n("Could not write to %1", m_fileName
);
124 QDomElement
MenuFile::findMenu(QDomElement elem
, const QString
&menuName
, bool create
)
126 QString menuNodeName
;
128 int i
= menuName
.indexOf('/');
131 menuNodeName
= menuName
.left(i
);
132 subMenuName
= menuName
.mid(i
+1);
136 menuNodeName
= menuName
;
139 return findMenu(elem
, subMenuName
, create
);
141 if (menuNodeName
.isEmpty())
144 QDomNode n
= elem
.firstChild();
147 QDomElement e
= n
.toElement(); // try to convert the node to an element.
148 if (e
.tagName() == MF_MENU
)
152 QDomNode n2
= e
.firstChild();
153 while ( !n2
.isNull() )
155 QDomElement e2
= n2
.toElement();
156 if (!e2
.isNull() && e2
.tagName() == MF_NAME
)
161 n2
= n2
.nextSibling();
164 if (name
== menuNodeName
)
166 if (subMenuName
.isEmpty())
169 return findMenu(e
, subMenuName
, create
);
176 return QDomElement();
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())
188 return findMenu(newElem
, subMenuName
, create
);
191 static QString
entryToDirId(const QString
&path
)
193 // See also KDesktopFile::locateLocal
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);
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();
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
);
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
)
245 static void purgeDeleted(QDomElement elem
)
247 // Remove any previous includes/excludes of appId
248 QDomNode n
= elem
.firstChild();
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
))
262 static void purgeLayout(QDomElement elem
)
264 // Remove any previous includes/excludes of appId
265 QDomNode n
= elem
.firstChild();
268 QDomNode next
= n
.nextSibling();
269 QDomElement e
= n
.toElement(); // try to convert the node to an element.
270 if (e
.tagName() == MF_LAYOUT
)
278 void MenuFile::addEntry(const QString
&menuName
, const QString
&menuId
)
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
)
306 QDomElement elem
= findMenu(m_doc
.documentElement(), menuName
, true);
310 QDomElement layoutNode
= m_doc
.createElement(MF_LAYOUT
);
311 elem
.appendChild(layoutNode
);
313 for(QStringList::ConstIterator it
= layout
.constBegin();
314 it
!= layout
.constEnd(); ++it
)
319 layoutNode
.appendChild(m_doc
.createElement(MF_SEPARATOR
));
323 QDomElement mergeNode
= m_doc
.createElement(MF_MERGE
);
324 mergeNode
.setAttribute("type", "menus");
325 layoutNode
.appendChild(mergeNode
);
329 QDomElement mergeNode
= m_doc
.createElement(MF_MERGE
);
330 mergeNode
.setAttribute("type", "files");
331 layoutNode
.appendChild(mergeNode
);
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
);
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
)
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
)
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
)
392 // Undelete the new menu
393 QDomElement elem
= findMenu(m_doc
.documentElement(), newMenu
, true);
395 elem
.appendChild(m_doc
.createElement(MF_NOTDELETED
));
397 // TODO: GET RID OF COMMON PART, IT BREAKS STUFF
399 QStringList oldMenuParts
= oldMenu
.split( '/');
400 QStringList newMenuParts
= newMenu
.split( '/');
401 QString commonMenuName
;
402 int max
= qMin(oldMenuParts
.count(), newMenuParts
.count());
406 if (oldMenuParts
[i
] != newMenuParts
[i
])
408 commonMenuName
+= '/' + oldMenuParts
[i
];
411 for(int j
= i
; j
< oldMenuParts
.count(); j
++)
415 oldMenuName
+= oldMenuParts
[j
];
418 for(int j
= i
; j
< newMenuParts
.count(); j
++)
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
)
444 QDomElement elem
= findMenu(m_doc
.documentElement(), menuName
, true);
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 '/'
469 for(int n
= 1; ++n
; )
471 if (findMenu(elem
, result
, false).isNull() && !excludeList
.contains(result
))
474 result
.truncate(trunc
);
475 result
.append(QString("-%1/").arg(n
));
477 return QString(); // Never reached
480 void MenuFile::performAction(const ActionAtom
*atom
)
485 addEntry(atom
->arg1
, atom
->arg2
);
488 removeEntry(atom
->arg1
, atom
->arg2
);
491 addMenu(atom
->arg1
, atom
->arg2
);
494 removeMenu(atom
->arg1
);
497 moveMenu(atom
->arg1
, atom
->arg2
);
502 MenuFile::ActionAtom
*MenuFile::pushAction(MenuFile::ActionType action
, const QString
&arg1
, const QString
&arg2
)
504 ActionAtom
*atom
= new ActionAtom
;
505 atom
->action
= action
;
508 m_actionList
.append(atom
);
512 void MenuFile::popAction(ActionAtom
*atom
)
514 if (m_actionList
.last() != atom
)
516 qWarning("MenuFile::popAction Error, action not last in list.");
519 m_actionList
.removeLast();
523 bool MenuFile::performAllActions()
525 Q_FOREACH(ActionAtom
*atom
, m_actionList
) {
526 performAction( 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();
549 bool MenuFile::dirty()
551 return (m_actionList
.count() != 0) || m_bDirty
;
554 void MenuFile::restoreMenuSystem( const QString
&filename
)
558 m_fileName
= filename
;
561 Q_FOREACH(ActionAtom
*atom
, m_actionList
) {
564 m_actionList
.clear();
566 m_removedEntries
.clear();