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 | |
50 | MenuFile::(const QString &file) |
51 | : m_fileName(file), m_bDirty(false) |
52 | { |
53 | load(); |
54 | } |
55 | |
56 | MenuFile::() |
57 | { |
58 | } |
59 | |
60 | bool MenuFile::() |
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 | |
88 | void MenuFile::() |
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 | |
95 | bool MenuFile::() |
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 | |
124 | QDomElement MenuFile::(QDomElement elem, const QString &, bool create) |
125 | { |
126 | QString ; |
127 | QString ; |
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 | |
191 | static 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 | |
210 | static 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 | |
245 | static 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 | |
262 | static 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 | |
278 | void MenuFile::(const QString &, const QString &) |
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 | |
302 | void MenuFile::(const QString &, 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 = 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 | |
356 | void MenuFile::(const QString &, const QString &) |
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 | |
378 | void MenuFile::(const QString &, const QString &) |
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 | |
388 | void MenuFile::(const QString &, const QString &) |
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 = oldMenu.split( '/'); |
400 | QStringList = newMenu.split( '/'); |
401 | QString ; |
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 ; |
411 | for(int j = i; j < oldMenuParts.count()-1; j++) |
412 | { |
413 | if (i != j) |
414 | oldMenuName += '/'; |
415 | oldMenuName += oldMenuParts[j]; |
416 | } |
417 | QString ; |
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 | |
440 | void MenuFile::(const QString &) |
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 | */ |
454 | QString MenuFile::(const QString &, const QString &, 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 | |
480 | void MenuFile::(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 | |
502 | MenuFile::ActionAtom *MenuFile::(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 | |
512 | void MenuFile::(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 | |
523 | bool MenuFile::() |
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 | |
549 | bool MenuFile::() const |
550 | { |
551 | return (m_actionList.count() != 0) || m_bDirty; |
552 | } |
553 | |
554 | void MenuFile::( 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 | |