1/*
2 * Copyright (C) 2003 Waldo Bastian <bastian@kde.org>
3 *
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.
7 *
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.
12 *
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.
16 *
17 */
18
19#include "menufile.h"
20
21#include <QFile>
22#include <QTextStream>
23#include <QRegExp>
24#include <QFileInfo>
25
26#include <KDebug>
27#include <KGlobal>
28#include <KLocale>
29#include <KStandardDirs>
30
31
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"
49
50MenuFile::MenuFile(const QString &file)
51 : m_fileName(file), m_bDirty(false)
52{
53 load();
54}
55
56MenuFile::~MenuFile()
57{
58}
59
60bool MenuFile::load()
61{
62 if (m_fileName.isEmpty())
63 return false;
64
65 QFile file( m_fileName );
66 if (!file.open( QIODevice::ReadOnly ))
67 {
68 if ( file.exists() )
69 kWarning() << "Could not read " << m_fileName ;
70 create();
71 return false;
72 }
73
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;
82 }
83 file.close();
84
85 return true;
86}
87
88void MenuFile::create()
89{
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);
93}
94
95bool MenuFile::save()
96{
97 QFile file( m_fileName );
98
99 if (!file.open( QIODevice::WriteOnly ))
100 {
101 kWarning() << "Could not write " << m_fileName ;
102 m_error = i18n("Could not write to %1", m_fileName);
103 return false;
104 }
105 QTextStream stream( &file );
106 stream.setCodec( "UTF-8" );
107
108 stream << m_doc.toString();
109
110 file.close();
111
112 if (file.error() != QFile::NoError)
113 {
114 kWarning() << "Could not close " << m_fileName ;
115 m_error = i18n("Could not write to %1", m_fileName);
116 return false;
117 }
118
119 m_bDirty = false;
120
121 return true;
122}
123
124QDomElement MenuFile::findMenu(QDomElement elem, const QString &menuName, bool create)
125{
126 QString menuNodeName;
127 QString subMenuName;
128 int i = menuName.indexOf('/');
129 if (i >= 0)
130 {
131 menuNodeName = menuName.left(i);
132 subMenuName = menuName.mid(i+1);
133 }
134 else
135 {
136 menuNodeName = menuName;
137 }
138 if (i == 0)
139 return findMenu(elem, subMenuName, create);
140
141 if (menuNodeName.isEmpty())
142 return elem;
143
144 QDomNode n = elem.firstChild();
145 while( !n.isNull() )
146 {
147 QDomElement e = n.toElement(); // try to convert the node to an element.
148 if (e.tagName() == MF_MENU)
149 {
150 QString name;
151
152 QDomNode n2 = e.firstChild();
153 while ( !n2.isNull() )
154 {
155 QDomElement e2 = n2.toElement();
156 if (!e2.isNull() && e2.tagName() == MF_NAME)
157 {
158 name = e2.text();
159 break;
160 }
161 n2 = n2.nextSibling();
162 }
163
164 if (name == menuNodeName)
165 {
166 if (subMenuName.isEmpty())
167 return e;
168 else
169 return findMenu(e, subMenuName, create);
170 }
171 }
172 n = n.nextSibling();
173 }
174
175 if (!create)
176 return QDomElement();
177
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);
184
185 if (subMenuName.isEmpty())
186 return newElem;
187 else
188 return findMenu(newElem, subMenuName, create);
189}
190
191static QString entryToDirId(const QString &path)
192{
193 // See also KDesktopFile::locateLocal
194 QString local;
195 if (QFileInfo(path).isAbsolute())
196 {
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);
200 }
201
202 if (local.isEmpty() || local.startsWith('/'))
203 {
204 // What now? Use filename only and hope for the best.
205 local = path.mid(path.lastIndexOf('/')+1);
206 }
207 return local;
208}
209
210static void purgeIncludesExcludes(QDomElement elem, const QString &appId, QDomElement &excludeNode, QDomElement &includeNode)
211{
212 // Remove any previous includes/excludes of appId
213 QDomNode n = elem.firstChild();
214 while( !n.isNull() )
215 {
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)
224 {
225 QDomNode n2 = e.firstChild();
226 while ( !n2.isNull() )
227 {
228 QDomNode next = n2.nextSibling();
229 QDomElement e2 = n2.toElement();
230 if (!e2.isNull() && e2.tagName() == MF_FILENAME)
231 {
232 if (e2.text() == appId)
233 {
234 e.removeChild(e2);
235 break;
236 }
237 }
238 n2 = next;
239 }
240 }
241 n = n.nextSibling();
242 }
243}
244
245static void purgeDeleted(QDomElement elem)
246{
247 // Remove any previous includes/excludes of appId
248 QDomNode n = elem.firstChild();
249 while( !n.isNull() )
250 {
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))
255 {
256 elem.removeChild(e);
257 }
258 n = next;
259 }
260}
261
262static void purgeLayout(QDomElement elem)
263{
264 // Remove any previous includes/excludes of appId
265 QDomNode n = elem.firstChild();
266 while( !n.isNull() )
267 {
268 QDomNode next = n.nextSibling();
269 QDomElement e = n.toElement(); // try to convert the node to an element.
270 if (e.tagName() == MF_LAYOUT)
271 {
272 elem.removeChild(e);
273 }
274 n = next;
275 }
276}
277
278void MenuFile::addEntry(const QString &menuName, const QString &menuId)
279{
280 m_bDirty = true;
281
282 m_removedEntries.removeAll(menuId);
283
284 QDomElement elem = findMenu(m_doc.documentElement(), menuName, true);
285
286 QDomElement excludeNode;
287 QDomElement includeNode;
288
289 purgeIncludesExcludes(elem, menuId, excludeNode, includeNode);
290
291 if (includeNode.isNull())
292 {
293 includeNode = m_doc.createElement(MF_INCLUDE);
294 elem.appendChild(includeNode);
295 }
296
297 QDomElement fileNode = m_doc.createElement(MF_FILENAME);
298 fileNode.appendChild(m_doc.createTextNode(menuId));
299 includeNode.appendChild(fileNode);
300}
301
302void MenuFile::setLayout(const QString &menuName, const QStringList &layout)
303{
304 m_bDirty = true;
305
306 QDomElement elem = findMenu(m_doc.documentElement(), menuName, true);
307
308 purgeLayout(elem);
309
310 QDomElement layoutNode = m_doc.createElement(MF_LAYOUT);
311 elem.appendChild(layoutNode);
312
313 for(QStringList::ConstIterator it = layout.constBegin();
314 it != layout.constEnd(); ++it)
315 {
316 QString li = *it;
317 if (li == ":S")
318 {
319 layoutNode.appendChild(m_doc.createElement(MF_SEPARATOR));
320 }
321 else if (li == ":M")
322 {
323 QDomElement mergeNode = m_doc.createElement(MF_MERGE);
324 mergeNode.setAttribute("type", "menus");
325 layoutNode.appendChild(mergeNode);
326 }
327 else if (li == ":F")
328 {
329 QDomElement mergeNode = m_doc.createElement(MF_MERGE);
330 mergeNode.setAttribute("type", "files");
331 layoutNode.appendChild(mergeNode);
332 }
333 else if (li == ":A")
334 {
335 QDomElement mergeNode = m_doc.createElement(MF_MERGE);
336 mergeNode.setAttribute("type", "all");
337 layoutNode.appendChild(mergeNode);
338 }
339 else if (li.endsWith('/'))
340 {
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);
345 }
346 else
347 {
348 QDomElement fileNode = m_doc.createElement(MF_FILENAME);
349 fileNode.appendChild(m_doc.createTextNode(li));
350 layoutNode.appendChild(fileNode);
351 }
352 }
353}
354
355
356void MenuFile::removeEntry(const QString &menuName, const QString &menuId)
357{
358 m_bDirty = true;
359 m_removedEntries.append(menuId);
360
361 QDomElement elem = findMenu(m_doc.documentElement(), menuName, true);
362
363 QDomElement excludeNode;
364 QDomElement includeNode;
365
366 purgeIncludesExcludes(elem, menuId, excludeNode, includeNode);
367
368 if (excludeNode.isNull())
369 {
370 excludeNode = m_doc.createElement(MF_EXCLUDE);
371 elem.appendChild(excludeNode);
372 }
373 QDomElement fileNode = m_doc.createElement(MF_FILENAME);
374 fileNode.appendChild(m_doc.createTextNode(menuId));
375 excludeNode.appendChild(fileNode);
376}
377
378void MenuFile::addMenu(const QString &menuName, const QString &menuFile)
379{
380 m_bDirty = true;
381 QDomElement elem = findMenu(m_doc.documentElement(), menuName, true);
382
383 QDomElement dirElem = m_doc.createElement(MF_DIRECTORY);
384 dirElem.appendChild(m_doc.createTextNode(entryToDirId(menuFile)));
385 elem.appendChild(dirElem);
386}
387
388void MenuFile::moveMenu(const QString &oldMenu, const QString &newMenu)
389{
390 m_bDirty = true;
391
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));
396
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++)
405 {
406 if (oldMenuParts[i] != newMenuParts[i])
407 break;
408 commonMenuName += '/' + oldMenuParts[i];
409 }
410 QString oldMenuName;
411 for(int j = i; j < oldMenuParts.count()-1; j++)
412 {
413 if (i != j)
414 oldMenuName += '/';
415 oldMenuName += oldMenuParts[j];
416 }
417 QString newMenuName;
418 for(int j = i; j < newMenuParts.count()-1; j++)
419 {
420 if (i != j)
421 newMenuName += '/';
422 newMenuName += newMenuParts[j];
423 }
424
425 if (oldMenuName == newMenuName) return; // Can happen
426
427 elem = findMenu(m_doc.documentElement(), commonMenuName, true);
428
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);
438}
439
440void MenuFile::removeMenu(const QString &menuName)
441{
442 m_bDirty = true;
443
444 QDomElement elem = findMenu(m_doc.documentElement(), menuName, true);
445
446 purgeDeleted(elem);
447 elem.appendChild(m_doc.createElement(MF_DELETED));
448}
449
450 /**
451 * Returns a unique menu-name for a new menu under @p menuName
452 * inspired by @p newMenu
453 */
454QString MenuFile::uniqueMenuName(const QString &menuName, const QString &newMenu, const QStringList & excludeList)
455{
456 QDomElement elem = findMenu(m_doc.documentElement(), menuName, false);
457
458 QString result = newMenu;
459 if (result.endsWith('/'))
460 result.truncate(result.length()-1);
461
462 QRegExp r("(.*)(?=-\\d+)");
463 result = (r.indexIn(result) > -1) ? r.cap(1) : result;
464
465 int trunc = result.length(); // Position of trailing '/'
466
467 result.append("/");
468
469 for(int n = 1; ++n; )
470 {
471 if (findMenu(elem, result, false).isNull() && !excludeList.contains(result))
472 return result;
473
474 result.truncate(trunc);
475 result.append(QString("-%1/").arg(n));
476 }
477 return QString(); // Never reached
478}
479
480void MenuFile::performAction(const ActionAtom *atom)
481{
482 switch(atom->action)
483 {
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;
499 }
500}
501
502MenuFile::ActionAtom *MenuFile::pushAction(MenuFile::ActionType action, const QString &arg1, const QString &arg2)
503{
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;
510}
511
512void MenuFile::popAction(ActionAtom *atom)
513{
514 if (m_actionList.last() != atom)
515 {
516 qWarning("MenuFile::popAction Error, action not last in list.");
517 return;
518 }
519 m_actionList.removeLast();
520 delete atom;
521}
522
523bool MenuFile::performAllActions()
524{
525 Q_FOREACH(ActionAtom *atom, m_actionList) {
526 performAction( atom );
527 delete atom;
528 }
529 m_actionList.clear();
530
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)
537 {
538 addEntry("/.hidden/", *it);
539 }
540
541 m_removedEntries.clear();
542
543 if (!m_bDirty)
544 return true;
545
546 return save();
547}
548
549bool MenuFile::dirty() const
550{
551 return (m_actionList.count() != 0) || m_bDirty;
552}
553
554void MenuFile::restoreMenuSystem( const QString &filename)
555{
556 m_error.clear();
557
558 m_fileName = filename;
559 m_doc.clear();
560 m_bDirty = false;
561 Q_FOREACH(ActionAtom *atom, m_actionList) {
562 delete atom;
563 }
564 m_actionList.clear();
565
566 m_removedEntries.clear();
567 create();
568}
569