1 """ CLass to edit XDG Menus """
4 from xdg.BaseDirectory import *
5 from xdg.Exceptions import *
6 from xdg.DesktopEntry import *
7 from xdg.Config import *
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
24 def __init__(self, menu=None, filename=None, root=False):
28 self.parse(menu, filename, root)
30 # fix for creating two menus with the same name on the fly
33 def parse(self, menu=None, filename=None, root=False):
37 if isinstance(menu, Menu):
40 self.menu = parse(menu)
45 self.filename = self.menu.Filename
47 self.filename = filename
49 self.filename = os.path.join(xdg_config_dirs[0], "menus", os.path.split(self.menu.Filename)[1])
52 self.doc = xml.dom.minidom.parse(self.filename)
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)
58 self.__remove_whilespace_nodes(self.doc)
61 self.__saveEntries(self.menu)
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)
68 self.__addEntry(parent, menuentry, after, before)
74 def createMenu(self, parent, name, genericname=None, comment=None, icon=None, after=None, before=None):
78 menu.Depth = parent.Depth + 1
79 menu.Layout = parent.DefaultLayout
80 menu.DefaultLayout = parent.DefaultLayout
82 menu = self.editMenu(menu, name, genericname, comment, icon)
84 self.__addEntry(parent, menu, after, before)
90 def createSeparator(self, parent, after=None, before=None):
91 separator = Separator(parent)
93 self.__addEntry(parent, separator, after, before)
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)
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)
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))
119 def moveSeparator(self, separator, parent, after=None, before=None):
120 self.__deleteEntry(parent, separator, after, before)
121 self.__addEntry(parent, separator, after, before)
127 def copyMenuEntry(self, menuentry, oldparent, newparent, after=None, before=None):
128 self.__addEntry(newparent, menuentry, after, before)
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
138 if not deskentry.hasKey("Name"):
139 deskentry.set("Name", name)
140 deskentry.set("Name", name, locale = True)
142 if not deskentry.hasKey("Comment"):
143 deskentry.set("Comment", comment)
144 deskentry.set("Comment", comment, locale = True)
146 if not deskentry.hasKey("GnericNe"):
147 deskentry.set("GenericName", genericname)
148 deskentry.set("GenericName", genericname, locale = True)
150 deskentry.set("Exec", command)
152 deskentry.set("Icon", icon)
155 deskentry.set("Terminal", "true")
156 elif terminal == False:
157 deskentry.set("Terminal", "false")
159 if nodisplay == True:
160 deskentry.set("NoDisplay", "true")
161 elif nodisplay == False:
162 deskentry.set("NoDisplay", "false")
165 deskentry.set("Hidden", "true")
166 elif hidden == False:
167 deskentry.set("Hidden", "false")
169 menuentry.updateAttributes()
171 if len(menuentry.Parents) > 0:
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):
186 filename = self.__getFileName(name, ".directory").replace("/", "")
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)
193 deskentry = menu.Directory.DesktopEntry
196 if not deskentry.hasKey("Name"):
197 deskentry.set("Name", name)
198 deskentry.set("Name", name, locale = True)
200 if not deskentry.hasKey("GenericName"):
201 deskentry.set("GenericName", genericname)
202 deskentry.set("GenericName", genericname, locale = True)
204 if not deskentry.hasKey("Comment"):
205 deskentry.set("Comment", comment)
206 deskentry.set("Comment", comment, locale = True)
208 deskentry.set("Icon", icon)
210 if nodisplay == True:
211 deskentry.set("NoDisplay", "true")
212 elif nodisplay == False:
213 deskentry.set("NoDisplay", "false")
216 deskentry.set("Hidden", "true")
217 elif hidden == False:
218 deskentry.set("Hidden", "false")
220 menu.Directory.updateAttributes()
222 if isinstance(menu.Parent, Menu):
227 def hideMenuEntry(self, menuentry):
228 self.editMenuEntry(menuentry, nodisplay = True)
230 def unhideMenuEntry(self, menuentry):
231 self.editMenuEntry(menuentry, nodisplay = False, hidden = False)
233 def hideMenu(self, menu):
234 self.editMenu(menu, nodisplay = True)
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)
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)
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)
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)
272 def revertMenu(self, menu):
273 if self.getAction(menu) == "revert":
274 self.__deleteFile(menu.Directory.DesktopEntry.filename)
275 menu.Directory = menu.Directory.Original
279 def deleteSeparator(self, separator):
280 self.__deleteEntry(separator.Parent, separator, after=True)
286 """ Private Stuff """
287 def getAction(self, entry):
288 if isinstance(entry, Menu):
289 if not isinstance(entry.Directory, MenuEntry):
291 elif entry.Directory.getType() == "Both":
293 elif entry.Directory.getType() == "User" \
294 and (len(entry.Submenus) + len(entry.MenuEntries)) == 0:
297 elif isinstance(entry, MenuEntry):
298 if entry.getType() == "Both":
300 elif entry.getType() == "User":
307 def __saveEntries(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):
315 elif isinstance(entry, Menu):
316 self.__saveEntries(entry)
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', '')))
325 def __getFileName(self, name, extension):
329 filename = name + extension
331 filename = name + "-" + str(postfix) + extension
332 if extension == ".desktop":
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)
345 def __getXmlMenu(self, path, create=True, element=None):
350 (name, path) = path.split("/", 1)
356 for node in self.__getXmlNodesByName("Menu", element):
357 for child in self.__getXmlNodesByName("Name", node):
358 if child.childNodes[0].nodeValue == name:
360 found = self.__getXmlMenu(path, create, node)
366 if not found and create == True:
367 node = self.__addXmlMenuElement(element, name)
369 found = self.__getXmlMenu(path, create, node)
375 def __addXmlMenuElement(self, element, name):
376 node = self.doc.createElement('Menu')
377 self.__addXmlTextElement(node, 'Name', name)
378 return element.appendChild(node)
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)
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)
393 node = self.doc.createElement(type)
394 node.appendChild(self.__addXmlTextElement(node, 'Filename', filename))
395 return element.appendChild(node)
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)
403 def __addXmlLayout(self, element, layout):
405 for node in self.__getXmlNodesByName("Layout", element):
406 element.removeChild(node)
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)
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:
429 def __addLayout(self, parent):
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
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"])
448 parent.Layout = layout
452 def __addEntry(self, parent, entry, after=None, before=None):
455 index = parent.Entries.index(after) + 1
457 index = parent.Entries.index(before)
458 parent.Entries.insert(index, entry)
460 parent.Entries.append(entry)
462 xml_parent = self.__getXmlMenu(parent.getPath(True, True))
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)
472 self.__addLayout(parent)
473 self.__addXmlLayout(xml_parent, parent.Layout)
475 def __deleteEntry(self, parent, entry, after=None, before=None):
476 parent.Entries.remove(entry)
478 xml_parent = self.__getXmlMenu(parent.getPath(True, True))
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)
488 self.__addLayout(parent)
489 self.__addXmlLayout(xml_parent, parent.Layout)
491 def __deleteFile(self, filename):
497 self.filenames.remove(filename)
501 def __remove_whilespace_nodes(self, node):
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)