vim line
[drlaunch] / src / xdg / MenuEditor.py
1 """ CLass to edit XDG Menus """
2
3 from xdg.Menu import *
4 from xdg.BaseDirectory import *
5 from xdg.Exceptions import *
6 from xdg.DesktopEntry import *
7 from xdg.Config import *
8
9 import xml.dom.minidom
10 import os
11 import re
12
13 # XML-Cleanups: Move / Exclude
14 # FIXME: proper reverte/delete
15 # FIXME: pass AppDirs/DirectoryDirs around in the edit/move functions
16 # FIXME: catch Exceptions
17 # FIXME: copy functions
18 # FIXME: More Layout stuff
19 # FIXME: unod/redo function / remove menu...
20 # FIXME: Advanced MenuEditing Stuff: LegacyDir/MergeFile
21 #        Complex Rules/Deleted/OnlyAllocated/AppDirs/DirectoryDirs
22
23 class MenuEditor:
24     def __init__(self, menu=None, filename=None, root=False):
25         self.menu = None
26         self.filename = None
27         self.doc = None
28         self.parse(menu, filename, root)
29
30         # fix for creating two menus with the same name on the fly
31         self.filenames = []
32
33     def parse(self, menu=None, filename=None, root=False):
34         if root == True:
35             setRootMode(True)
36
37         if isinstance(menu, Menu):
38             self.menu = menu
39         elif menu:
40             self.menu = parse(menu)
41         else:
42             self.menu = parse()
43
44         if root == True:
45             self.filename = self.menu.Filename
46         elif filename:
47             self.filename = filename
48         else:
49             self.filename = os.path.join(xdg_config_dirs[0], "menus", os.path.split(self.menu.Filename)[1])
50
51         try:
52             self.doc = xml.dom.minidom.parse(self.filename)
53         except IOError:
54             self.doc = xml.dom.minidom.parseString('<!DOCTYPE Menu PUBLIC "-//freedesktop//DTD Menu 1.0//EN" "http://standards.freedesktop.org/menu-spec/menu-1.0.dtd"><Menu><Name>Applications</Name><MergeFile type="parent">'+self.menu.Filename+'</MergeFile></Menu>')
55         except xml.parsers.expat.ExpatError:
56             raise ParsingError('Not a valid .menu file', self.filename)
57
58         self.__remove_whilespace_nodes(self.doc)
59
60     def save(self):
61         self.__saveEntries(self.menu)
62         self.__saveMenu()
63
64     def createMenuEntry(self, parent, name, command=None, genericname=None, comment=None, icon=None, terminal=None, after=None, before=None):
65         menuentry = MenuEntry(self.__getFileName(name, ".desktop"))
66         menuentry = self.editMenuEntry(menuentry, name, genericname, comment, command, icon, terminal)
67
68         self.__addEntry(parent, menuentry, after, before)
69
70         sort(self.menu)
71
72         return menuentry
73
74     def createMenu(self, parent, name, genericname=None, comment=None, icon=None, after=None, before=None):
75         menu = Menu()
76
77         menu.Parent = parent
78         menu.Depth = parent.Depth + 1
79         menu.Layout = parent.DefaultLayout
80         menu.DefaultLayout = parent.DefaultLayout
81
82         menu = self.editMenu(menu, name, genericname, comment, icon)
83
84         self.__addEntry(parent, menu, after, before)
85
86         sort(self.menu)
87
88         return menu
89
90     def createSeparator(self, parent, after=None, before=None):
91         separator = Separator(parent)
92
93         self.__addEntry(parent, separator, after, before)
94
95         sort(self.menu)
96
97         return separator
98
99     def moveMenuEntry(self, menuentry, oldparent, newparent, after=None, before=None):
100         self.__deleteEntry(oldparent, menuentry, after, before)
101         self.__addEntry(newparent, menuentry, after, before)
102
103         sort(self.menu)
104
105         return menuentry
106
107     def moveMenu(self, menu, oldparent, newparent, after=None, before=None):
108         self.__deleteEntry(oldparent, menu, after, before)
109         self.__addEntry(newparent, menu, after, before)
110
111         root_menu = self.__getXmlMenu(self.menu.Name)
112         if oldparent.getPath(True) != newparent.getPath(True):
113             self.__addXmlMove(root_menu, os.path.join(oldparent.getPath(True), menu.Name), os.path.join(newparent.getPath(True), menu.Name))
114
115         sort(self.menu)
116
117         return menu
118
119     def moveSeparator(self, separator, parent, after=None, before=None):
120         self.__deleteEntry(parent, separator, after, before)
121         self.__addEntry(parent, separator, after, before)
122
123         sort(self.menu)
124
125         return separator
126
127     def copyMenuEntry(self, menuentry, oldparent, newparent, after=None, before=None):
128         self.__addEntry(newparent, menuentry, after, before)
129
130         sort(self.menu)
131
132         return menuentry
133
134     def editMenuEntry(self, menuentry, name=None, genericname=None, comment=None, command=None, icon=None, terminal=None, nodisplay=None, hidden=None):
135         deskentry = menuentry.DesktopEntry
136
137         if name:
138             if not deskentry.hasKey("Name"):
139                 deskentry.set("Name", name)
140             deskentry.set("Name", name, locale = True)
141         if comment:
142             if not deskentry.hasKey("Comment"):
143                 deskentry.set("Comment", comment)
144             deskentry.set("Comment", comment, locale = True)
145         if genericname:
146             if not deskentry.hasKey("GnericNe"):
147                 deskentry.set("GenericName", genericname)
148             deskentry.set("GenericName", genericname, locale = True)
149         if command:
150             deskentry.set("Exec", command)
151         if icon:
152             deskentry.set("Icon", icon)
153
154         if terminal == True:
155             deskentry.set("Terminal", "true")
156         elif terminal == False:
157             deskentry.set("Terminal", "false")
158
159         if nodisplay == True:
160             deskentry.set("NoDisplay", "true")
161         elif nodisplay == False:
162             deskentry.set("NoDisplay", "false")
163
164         if hidden == True:
165             deskentry.set("Hidden", "true")
166         elif hidden == False:
167             deskentry.set("Hidden", "false")
168
169         menuentry.updateAttributes()
170
171         if len(menuentry.Parents) > 0:
172             sort(self.menu)
173
174         return menuentry
175
176     def editMenu(self, menu, name=None, genericname=None, comment=None, icon=None, nodisplay=None, hidden=None):
177         # Hack for legacy dirs
178         if isinstance(menu.Directory, MenuEntry) and menu.Directory.Filename == ".directory":
179             xml_menu = self.__getXmlMenu(menu.getPath(True, True))
180             self.__addXmlTextElement(xml_menu, 'Directory', menu.Name + ".directory")
181             menu.Directory.setAttributes(menu.Name + ".directory")
182         # Hack for New Entries
183         elif not isinstance(menu.Directory, MenuEntry):
184             if not name:
185                 name = menu.Name
186             filename = self.__getFileName(name, ".directory").replace("/", "")
187             if not menu.Name:
188                 menu.Name = filename.replace(".directory", "")
189             xml_menu = self.__getXmlMenu(menu.getPath(True, True))
190             self.__addXmlTextElement(xml_menu, 'Directory', filename)
191             menu.Directory = MenuEntry(filename)
192
193         deskentry = menu.Directory.DesktopEntry
194
195         if name:
196             if not deskentry.hasKey("Name"):
197                 deskentry.set("Name", name)
198             deskentry.set("Name", name, locale = True)
199         if genericname:
200             if not deskentry.hasKey("GenericName"):
201                 deskentry.set("GenericName", genericname)
202             deskentry.set("GenericName", genericname, locale = True)
203         if comment:
204             if not deskentry.hasKey("Comment"):
205                 deskentry.set("Comment", comment)
206             deskentry.set("Comment", comment, locale = True)
207         if icon:
208             deskentry.set("Icon", icon)
209
210         if nodisplay == True:
211             deskentry.set("NoDisplay", "true")
212         elif nodisplay == False:
213             deskentry.set("NoDisplay", "false")
214
215         if hidden == True:
216             deskentry.set("Hidden", "true")
217         elif hidden == False:
218             deskentry.set("Hidden", "false")
219
220         menu.Directory.updateAttributes()
221
222         if isinstance(menu.Parent, Menu):
223             sort(self.menu)
224
225         return menu
226
227     def hideMenuEntry(self, menuentry):
228         self.editMenuEntry(menuentry, nodisplay = True)
229
230     def unhideMenuEntry(self, menuentry):
231         self.editMenuEntry(menuentry, nodisplay = False, hidden = False)
232
233     def hideMenu(self, menu):
234         self.editMenu(menu, nodisplay = True)
235
236     def unhideMenu(self, menu):
237         self.editMenu(menu, nodisplay = False, hidden = False)
238         xml_menu = self.__getXmlMenu(menu.getPath(True,True), False)
239         for node in self.__getXmlNodesByName(["Deleted", "NotDeleted"], xml_menu):
240             node.parentNode.removeChild(node)
241
242     def deleteMenuEntry(self, menuentry):
243         if self.getAction(menuentry) == "delete":
244             self.__deleteFile(menuentry.DesktopEntry.filename)
245             for parent in menuentry.Parents:
246                 self.__deleteEntry(parent, menuentry)
247             sort(self.menu)
248         return menuentry
249
250     def revertMenuEntry(self, menuentry):
251         if self.getAction(menuentry) == "revert":
252             self.__deleteFile(menuentry.DesktopEntry.filename)
253             menuentry.Original.Parents = []
254             for parent in menuentry.Parents:
255                 index = parent.Entries.index(menuentry)
256                 parent.Entries[index] = menuentry.Original
257                 index = parent.MenuEntries.index(menuentry)
258                 parent.MenuEntries[index] = menuentry.Original
259                 menuentry.Original.Parents.append(parent)
260             sort(self.menu)
261         return menuentry
262
263     def deleteMenu(self, menu):
264         if self.getAction(menu) == "delete":
265             self.__deleteFile(menu.Directory.DesktopEntry.filename)
266             self.__deleteEntry(menu.Parent, menu)
267             xml_menu = self.__getXmlMenu(menu.getPath(True, True))
268             xml_menu.parentNode.removeChild(xml_menu)
269             sort(self.menu)
270         return menu
271
272     def revertMenu(self, menu):
273         if self.getAction(menu) == "revert":
274             self.__deleteFile(menu.Directory.DesktopEntry.filename)
275             menu.Directory = menu.Directory.Original
276             sort(self.menu)
277         return menu
278
279     def deleteSeparator(self, separator):
280         self.__deleteEntry(separator.Parent, separator, after=True)
281
282         sort(self.menu)
283
284         return separator
285
286     """ Private Stuff """
287     def getAction(self, entry):
288         if isinstance(entry, Menu):
289             if not isinstance(entry.Directory, MenuEntry):
290                 return "none"
291             elif entry.Directory.getType() == "Both":
292                 return "revert"
293             elif entry.Directory.getType() == "User" \
294             and (len(entry.Submenus) + len(entry.MenuEntries)) == 0:
295                 return "delete"
296
297         elif isinstance(entry, MenuEntry):
298             if entry.getType() == "Both":
299                 return "revert"
300             elif entry.getType() == "User":
301                 return "delete"
302             else:
303                 return "none"
304
305         return "none"
306
307     def __saveEntries(self, menu):
308         if not menu:
309             menu = self.menu
310         if isinstance(menu.Directory, MenuEntry):
311             menu.Directory.save()
312         for entry in menu.getEntries(hidden=True):
313             if isinstance(entry, MenuEntry):
314                 entry.save()
315             elif isinstance(entry, Menu):
316                 self.__saveEntries(entry)
317
318     def __saveMenu(self):
319         if not os.path.isdir(os.path.dirname(self.filename)):
320             os.makedirs(os.path.dirname(self.filename))
321         fd = open(self.filename, 'w')
322         fd.write(re.sub("\n[\s]*([^\n<]*)\n[\s]*</", "\\1</", self.doc.toprettyxml().replace('<?xml version="1.0" ?>\n', '')))
323         fd.close()
324
325     def __getFileName(self, name, extension):
326         postfix = 0
327         while 1:
328             if postfix == 0:
329                 filename = name + extension
330             else:
331                 filename = name + "-" + str(postfix) + extension
332             if extension == ".desktop":
333                 dir = "applications"
334             elif extension == ".directory":
335                 dir = "desktop-directories"
336             if not filename in self.filenames and not \
337                 os.path.isfile(os.path.join(xdg_data_dirs[0], dir, filename)):
338                 self.filenames.append(filename)
339                 break
340             else:
341                 postfix += 1
342
343         return filename
344
345     def __getXmlMenu(self, path, create=True, element=None):
346         if not element:
347             element = self.doc
348
349         if "/" in path:
350             (name, path) = path.split("/", 1)
351         else:
352             name = path
353             path = ""
354
355         found = None
356         for node in self.__getXmlNodesByName("Menu", element):
357             for child in self.__getXmlNodesByName("Name", node):
358                 if child.childNodes[0].nodeValue == name:
359                     if path:
360                         found = self.__getXmlMenu(path, create, node)
361                     else:
362                         found = node
363                     break
364             if found:
365                 break
366         if not found and create == True:
367             node = self.__addXmlMenuElement(element, name)
368             if path:
369                 found = self.__getXmlMenu(path, create, node)
370             else:
371                 found = node
372
373         return found
374
375     def __addXmlMenuElement(self, element, name):
376         node = self.doc.createElement('Menu')
377         self.__addXmlTextElement(node, 'Name', name)
378         return element.appendChild(node)
379
380     def __addXmlTextElement(self, element, name, text):
381         node = self.doc.createElement(name)
382         text = self.doc.createTextNode(text)
383         node.appendChild(text)
384         return element.appendChild(node)
385
386     def __addXmlFilename(self, element, filename, type = "Include"):
387         # remove old filenames
388         for node in self.__getXmlNodesByName(["Include", "Exclude"], element):
389             if node.childNodes[0].nodeName == "Filename" and node.childNodes[0].childNodes[0].nodeValue == filename:
390                 element.removeChild(node)
391
392         # add new filename
393         node = self.doc.createElement(type)
394         node.appendChild(self.__addXmlTextElement(node, 'Filename', filename))
395         return element.appendChild(node)
396
397     def __addXmlMove(self, element, old, new):
398         node = self.doc.createElement("Move")
399         node.appendChild(self.__addXmlTextElement(node, 'Old', old))
400         node.appendChild(self.__addXmlTextElement(node, 'New', new))
401         return element.appendChild(node)
402
403     def __addXmlLayout(self, element, layout):
404         # remove old layout
405         for node in self.__getXmlNodesByName("Layout", element):
406             element.removeChild(node)
407
408         # add new layout
409         node = self.doc.createElement("Layout")
410         for order in layout.order:
411             if order[0] == "Separator":
412                 child = self.doc.createElement("Separator")
413                 node.appendChild(child)
414             elif order[0] == "Filename":
415                 child = self.__addXmlTextElement(node, "Filename", order[1])
416             elif order[0] == "Menuname":
417                 child = self.__addXmlTextElement(node, "Menuname", order[1])
418             elif order[0] == "Merge":
419                 child = self.doc.createElement("Merge")
420                 child.setAttribute("type", order[1])
421                 node.appendChild(child)
422         return element.appendChild(node)
423
424     def __getXmlNodesByName(self, name, element):
425         for child in element.childNodes:
426             if child.nodeType == xml.dom.Node.ELEMENT_NODE and child.nodeName in name:
427                 yield child
428
429     def __addLayout(self, parent):
430         layout = Layout()
431         layout.order = []
432         layout.show_empty = parent.Layout.show_empty
433         layout.inline = parent.Layout.inline
434         layout.inline_header = parent.Layout.inline_header
435         layout.inline_alias = parent.Layout.inline_alias
436         layout.inline_limit = parent.Layout.inline_limit
437
438         layout.order.append(["Merge", "menus"])
439         for entry in parent.Entries:
440             if isinstance(entry, Menu):
441                 layout.parseMenuname(entry.Name)
442             elif isinstance(entry, MenuEntry):
443                 layout.parseFilename(entry.DesktopFileID)
444             elif isinstance(entry, Separator):
445                 layout.parseSeparator()
446         layout.order.append(["Merge", "files"])
447
448         parent.Layout = layout
449
450         return layout
451
452     def __addEntry(self, parent, entry, after=None, before=None):
453         if after or before:
454             if after:
455                 index = parent.Entries.index(after) + 1
456             elif before:
457                 index = parent.Entries.index(before)
458             parent.Entries.insert(index, entry)
459         else:
460             parent.Entries.append(entry)
461
462         xml_parent = self.__getXmlMenu(parent.getPath(True, True))
463
464         if isinstance(entry, MenuEntry):
465             parent.MenuEntries.append(entry)
466             entry.Parents.append(parent)
467             self.__addXmlFilename(xml_parent, entry.DesktopFileID, "Include")
468         elif isinstance(entry, Menu):
469             parent.addSubmenu(entry)
470
471         if after or before:
472             self.__addLayout(parent)
473             self.__addXmlLayout(xml_parent, parent.Layout)
474
475     def __deleteEntry(self, parent, entry, after=None, before=None):
476         parent.Entries.remove(entry)
477
478         xml_parent = self.__getXmlMenu(parent.getPath(True, True))
479
480         if isinstance(entry, MenuEntry):
481             entry.Parents.remove(parent)
482             parent.MenuEntries.remove(entry)
483             self.__addXmlFilename(xml_parent, entry.DesktopFileID, "Exclude")
484         elif isinstance(entry, Menu):
485             parent.Submenus.remove(entry)
486
487         if after or before:
488             self.__addLayout(parent)
489             self.__addXmlLayout(xml_parent, parent.Layout)
490
491     def __deleteFile(self, filename):
492         try:
493             os.remove(filename)
494         except OSError:
495             pass
496         try:
497             self.filenames.remove(filename)
498         except ValueError:
499             pass
500
501     def __remove_whilespace_nodes(self, node):
502         remove_list = []
503         for child in node.childNodes:
504             if child.nodeType == xml.dom.minidom.Node.TEXT_NODE:
505                 child.data = child.data.strip()
506                 if not child.data.strip():
507                     remove_list.append(child)
508             elif child.hasChildNodes():
509                 self.__remove_whilespace_nodes(child)
510         for node in remove_list:
511             node.parentNode.removeChild(node)