--- /dev/null
+""" CLass to edit XDG Menus """
+
+from xdg.Menu import *
+from xdg.BaseDirectory import *
+from xdg.Exceptions import *
+from xdg.DesktopEntry import *
+from xdg.Config import *
+
+import xml.dom.minidom
+import os
+import re
+
+# XML-Cleanups: Move / Exclude
+# FIXME: proper reverte/delete
+# FIXME: pass AppDirs/DirectoryDirs around in the edit/move functions
+# FIXME: catch Exceptions
+# FIXME: copy functions
+# FIXME: More Layout stuff
+# FIXME: unod/redo function / remove menu...
+# FIXME: Advanced MenuEditing Stuff: LegacyDir/MergeFile
+# Complex Rules/Deleted/OnlyAllocated/AppDirs/DirectoryDirs
+
+class MenuEditor:
+ def __init__(self, menu=None, filename=None, root=False):
+ self.menu = None
+ self.filename = None
+ self.doc = None
+ self.parse(menu, filename, root)
+
+ # fix for creating two menus with the same name on the fly
+ self.filenames = []
+
+ def parse(self, menu=None, filename=None, root=False):
+ if root == True:
+ setRootMode(True)
+
+ if isinstance(menu, Menu):
+ self.menu = menu
+ elif menu:
+ self.menu = parse(menu)
+ else:
+ self.menu = parse()
+
+ if root == True:
+ self.filename = self.menu.Filename
+ elif filename:
+ self.filename = filename
+ else:
+ self.filename = os.path.join(xdg_config_dirs[0], "menus", os.path.split(self.menu.Filename)[1])
+
+ try:
+ self.doc = xml.dom.minidom.parse(self.filename)
+ except IOError:
+ 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>')
+ except xml.parsers.expat.ExpatError:
+ raise ParsingError('Not a valid .menu file', self.filename)
+
+ self.__remove_whilespace_nodes(self.doc)
+
+ def save(self):
+ self.__saveEntries(self.menu)
+ self.__saveMenu()
+
+ def createMenuEntry(self, parent, name, command=None, genericname=None, comment=None, icon=None, terminal=None, after=None, before=None):
+ menuentry = MenuEntry(self.__getFileName(name, ".desktop"))
+ menuentry = self.editMenuEntry(menuentry, name, genericname, comment, command, icon, terminal)
+
+ self.__addEntry(parent, menuentry, after, before)
+
+ sort(self.menu)
+
+ return menuentry
+
+ def createMenu(self, parent, name, genericname=None, comment=None, icon=None, after=None, before=None):
+ menu = Menu()
+
+ menu.Parent = parent
+ menu.Depth = parent.Depth + 1
+ menu.Layout = parent.DefaultLayout
+ menu.DefaultLayout = parent.DefaultLayout
+
+ menu = self.editMenu(menu, name, genericname, comment, icon)
+
+ self.__addEntry(parent, menu, after, before)
+
+ sort(self.menu)
+
+ return menu
+
+ def createSeparator(self, parent, after=None, before=None):
+ separator = Separator(parent)
+
+ self.__addEntry(parent, separator, after, before)
+
+ sort(self.menu)
+
+ return separator
+
+ def moveMenuEntry(self, menuentry, oldparent, newparent, after=None, before=None):
+ self.__deleteEntry(oldparent, menuentry, after, before)
+ self.__addEntry(newparent, menuentry, after, before)
+
+ sort(self.menu)
+
+ return menuentry
+
+ def moveMenu(self, menu, oldparent, newparent, after=None, before=None):
+ self.__deleteEntry(oldparent, menu, after, before)
+ self.__addEntry(newparent, menu, after, before)
+
+ root_menu = self.__getXmlMenu(self.menu.Name)
+ if oldparent.getPath(True) != newparent.getPath(True):
+ self.__addXmlMove(root_menu, os.path.join(oldparent.getPath(True), menu.Name), os.path.join(newparent.getPath(True), menu.Name))
+
+ sort(self.menu)
+
+ return menu
+
+ def moveSeparator(self, separator, parent, after=None, before=None):
+ self.__deleteEntry(parent, separator, after, before)
+ self.__addEntry(parent, separator, after, before)
+
+ sort(self.menu)
+
+ return separator
+
+ def copyMenuEntry(self, menuentry, oldparent, newparent, after=None, before=None):
+ self.__addEntry(newparent, menuentry, after, before)
+
+ sort(self.menu)
+
+ return menuentry
+
+ def editMenuEntry(self, menuentry, name=None, genericname=None, comment=None, command=None, icon=None, terminal=None, nodisplay=None, hidden=None):
+ deskentry = menuentry.DesktopEntry
+
+ if name:
+ if not deskentry.hasKey("Name"):
+ deskentry.set("Name", name)
+ deskentry.set("Name", name, locale = True)
+ if comment:
+ if not deskentry.hasKey("Comment"):
+ deskentry.set("Comment", comment)
+ deskentry.set("Comment", comment, locale = True)
+ if genericname:
+ if not deskentry.hasKey("GnericNe"):
+ deskentry.set("GenericName", genericname)
+ deskentry.set("GenericName", genericname, locale = True)
+ if command:
+ deskentry.set("Exec", command)
+ if icon:
+ deskentry.set("Icon", icon)
+
+ if terminal == True:
+ deskentry.set("Terminal", "true")
+ elif terminal == False:
+ deskentry.set("Terminal", "false")
+
+ if nodisplay == True:
+ deskentry.set("NoDisplay", "true")
+ elif nodisplay == False:
+ deskentry.set("NoDisplay", "false")
+
+ if hidden == True:
+ deskentry.set("Hidden", "true")
+ elif hidden == False:
+ deskentry.set("Hidden", "false")
+
+ menuentry.updateAttributes()
+
+ if len(menuentry.Parents) > 0:
+ sort(self.menu)
+
+ return menuentry
+
+ def editMenu(self, menu, name=None, genericname=None, comment=None, icon=None, nodisplay=None, hidden=None):
+ # Hack for legacy dirs
+ if isinstance(menu.Directory, MenuEntry) and menu.Directory.Filename == ".directory":
+ xml_menu = self.__getXmlMenu(menu.getPath(True, True))
+ self.__addXmlTextElement(xml_menu, 'Directory', menu.Name + ".directory")
+ menu.Directory.setAttributes(menu.Name + ".directory")
+ # Hack for New Entries
+ elif not isinstance(menu.Directory, MenuEntry):
+ if not name:
+ name = menu.Name
+ filename = self.__getFileName(name, ".directory").replace("/", "")
+ if not menu.Name:
+ menu.Name = filename.replace(".directory", "")
+ xml_menu = self.__getXmlMenu(menu.getPath(True, True))
+ self.__addXmlTextElement(xml_menu, 'Directory', filename)
+ menu.Directory = MenuEntry(filename)
+
+ deskentry = menu.Directory.DesktopEntry
+
+ if name:
+ if not deskentry.hasKey("Name"):
+ deskentry.set("Name", name)
+ deskentry.set("Name", name, locale = True)
+ if genericname:
+ if not deskentry.hasKey("GenericName"):
+ deskentry.set("GenericName", genericname)
+ deskentry.set("GenericName", genericname, locale = True)
+ if comment:
+ if not deskentry.hasKey("Comment"):
+ deskentry.set("Comment", comment)
+ deskentry.set("Comment", comment, locale = True)
+ if icon:
+ deskentry.set("Icon", icon)
+
+ if nodisplay == True:
+ deskentry.set("NoDisplay", "true")
+ elif nodisplay == False:
+ deskentry.set("NoDisplay", "false")
+
+ if hidden == True:
+ deskentry.set("Hidden", "true")
+ elif hidden == False:
+ deskentry.set("Hidden", "false")
+
+ menu.Directory.updateAttributes()
+
+ if isinstance(menu.Parent, Menu):
+ sort(self.menu)
+
+ return menu
+
+ def hideMenuEntry(self, menuentry):
+ self.editMenuEntry(menuentry, nodisplay = True)
+
+ def unhideMenuEntry(self, menuentry):
+ self.editMenuEntry(menuentry, nodisplay = False, hidden = False)
+
+ def hideMenu(self, menu):
+ self.editMenu(menu, nodisplay = True)
+
+ def unhideMenu(self, menu):
+ self.editMenu(menu, nodisplay = False, hidden = False)
+ xml_menu = self.__getXmlMenu(menu.getPath(True,True), False)
+ for node in self.__getXmlNodesByName(["Deleted", "NotDeleted"], xml_menu):
+ node.parentNode.removeChild(node)
+
+ def deleteMenuEntry(self, menuentry):
+ if self.getAction(menuentry) == "delete":
+ self.__deleteFile(menuentry.DesktopEntry.filename)
+ for parent in menuentry.Parents:
+ self.__deleteEntry(parent, menuentry)
+ sort(self.menu)
+ return menuentry
+
+ def revertMenuEntry(self, menuentry):
+ if self.getAction(menuentry) == "revert":
+ self.__deleteFile(menuentry.DesktopEntry.filename)
+ menuentry.Original.Parents = []
+ for parent in menuentry.Parents:
+ index = parent.Entries.index(menuentry)
+ parent.Entries[index] = menuentry.Original
+ index = parent.MenuEntries.index(menuentry)
+ parent.MenuEntries[index] = menuentry.Original
+ menuentry.Original.Parents.append(parent)
+ sort(self.menu)
+ return menuentry
+
+ def deleteMenu(self, menu):
+ if self.getAction(menu) == "delete":
+ self.__deleteFile(menu.Directory.DesktopEntry.filename)
+ self.__deleteEntry(menu.Parent, menu)
+ xml_menu = self.__getXmlMenu(menu.getPath(True, True))
+ xml_menu.parentNode.removeChild(xml_menu)
+ sort(self.menu)
+ return menu
+
+ def revertMenu(self, menu):
+ if self.getAction(menu) == "revert":
+ self.__deleteFile(menu.Directory.DesktopEntry.filename)
+ menu.Directory = menu.Directory.Original
+ sort(self.menu)
+ return menu
+
+ def deleteSeparator(self, separator):
+ self.__deleteEntry(separator.Parent, separator, after=True)
+
+ sort(self.menu)
+
+ return separator
+
+ """ Private Stuff """
+ def getAction(self, entry):
+ if isinstance(entry, Menu):
+ if not isinstance(entry.Directory, MenuEntry):
+ return "none"
+ elif entry.Directory.getType() == "Both":
+ return "revert"
+ elif entry.Directory.getType() == "User" \
+ and (len(entry.Submenus) + len(entry.MenuEntries)) == 0:
+ return "delete"
+
+ elif isinstance(entry, MenuEntry):
+ if entry.getType() == "Both":
+ return "revert"
+ elif entry.getType() == "User":
+ return "delete"
+ else:
+ return "none"
+
+ return "none"
+
+ def __saveEntries(self, menu):
+ if not menu:
+ menu = self.menu
+ if isinstance(menu.Directory, MenuEntry):
+ menu.Directory.save()
+ for entry in menu.getEntries(hidden=True):
+ if isinstance(entry, MenuEntry):
+ entry.save()
+ elif isinstance(entry, Menu):
+ self.__saveEntries(entry)
+
+ def __saveMenu(self):
+ if not os.path.isdir(os.path.dirname(self.filename)):
+ os.makedirs(os.path.dirname(self.filename))
+ fd = open(self.filename, 'w')
+ fd.write(re.sub("\n[\s]*([^\n<]*)\n[\s]*</", "\\1</", self.doc.toprettyxml().replace('<?xml version="1.0" ?>\n', '')))
+ fd.close()
+
+ def __getFileName(self, name, extension):
+ postfix = 0
+ while 1:
+ if postfix == 0:
+ filename = name + extension
+ else:
+ filename = name + "-" + str(postfix) + extension
+ if extension == ".desktop":
+ dir = "applications"
+ elif extension == ".directory":
+ dir = "desktop-directories"
+ if not filename in self.filenames and not \
+ os.path.isfile(os.path.join(xdg_data_dirs[0], dir, filename)):
+ self.filenames.append(filename)
+ break
+ else:
+ postfix += 1
+
+ return filename
+
+ def __getXmlMenu(self, path, create=True, element=None):
+ if not element:
+ element = self.doc
+
+ if "/" in path:
+ (name, path) = path.split("/", 1)
+ else:
+ name = path
+ path = ""
+
+ found = None
+ for node in self.__getXmlNodesByName("Menu", element):
+ for child in self.__getXmlNodesByName("Name", node):
+ if child.childNodes[0].nodeValue == name:
+ if path:
+ found = self.__getXmlMenu(path, create, node)
+ else:
+ found = node
+ break
+ if found:
+ break
+ if not found and create == True:
+ node = self.__addXmlMenuElement(element, name)
+ if path:
+ found = self.__getXmlMenu(path, create, node)
+ else:
+ found = node
+
+ return found
+
+ def __addXmlMenuElement(self, element, name):
+ node = self.doc.createElement('Menu')
+ self.__addXmlTextElement(node, 'Name', name)
+ return element.appendChild(node)
+
+ def __addXmlTextElement(self, element, name, text):
+ node = self.doc.createElement(name)
+ text = self.doc.createTextNode(text)
+ node.appendChild(text)
+ return element.appendChild(node)
+
+ def __addXmlFilename(self, element, filename, type = "Include"):
+ # remove old filenames
+ for node in self.__getXmlNodesByName(["Include", "Exclude"], element):
+ if node.childNodes[0].nodeName == "Filename" and node.childNodes[0].childNodes[0].nodeValue == filename:
+ element.removeChild(node)
+
+ # add new filename
+ node = self.doc.createElement(type)
+ node.appendChild(self.__addXmlTextElement(node, 'Filename', filename))
+ return element.appendChild(node)
+
+ def __addXmlMove(self, element, old, new):
+ node = self.doc.createElement("Move")
+ node.appendChild(self.__addXmlTextElement(node, 'Old', old))
+ node.appendChild(self.__addXmlTextElement(node, 'New', new))
+ return element.appendChild(node)
+
+ def __addXmlLayout(self, element, layout):
+ # remove old layout
+ for node in self.__getXmlNodesByName("Layout", element):
+ element.removeChild(node)
+
+ # add new layout
+ node = self.doc.createElement("Layout")
+ for order in layout.order:
+ if order[0] == "Separator":
+ child = self.doc.createElement("Separator")
+ node.appendChild(child)
+ elif order[0] == "Filename":
+ child = self.__addXmlTextElement(node, "Filename", order[1])
+ elif order[0] == "Menuname":
+ child = self.__addXmlTextElement(node, "Menuname", order[1])
+ elif order[0] == "Merge":
+ child = self.doc.createElement("Merge")
+ child.setAttribute("type", order[1])
+ node.appendChild(child)
+ return element.appendChild(node)
+
+ def __getXmlNodesByName(self, name, element):
+ for child in element.childNodes:
+ if child.nodeType == xml.dom.Node.ELEMENT_NODE and child.nodeName in name:
+ yield child
+
+ def __addLayout(self, parent):
+ layout = Layout()
+ layout.order = []
+ layout.show_empty = parent.Layout.show_empty
+ layout.inline = parent.Layout.inline
+ layout.inline_header = parent.Layout.inline_header
+ layout.inline_alias = parent.Layout.inline_alias
+ layout.inline_limit = parent.Layout.inline_limit
+
+ layout.order.append(["Merge", "menus"])
+ for entry in parent.Entries:
+ if isinstance(entry, Menu):
+ layout.parseMenuname(entry.Name)
+ elif isinstance(entry, MenuEntry):
+ layout.parseFilename(entry.DesktopFileID)
+ elif isinstance(entry, Separator):
+ layout.parseSeparator()
+ layout.order.append(["Merge", "files"])
+
+ parent.Layout = layout
+
+ return layout
+
+ def __addEntry(self, parent, entry, after=None, before=None):
+ if after or before:
+ if after:
+ index = parent.Entries.index(after) + 1
+ elif before:
+ index = parent.Entries.index(before)
+ parent.Entries.insert(index, entry)
+ else:
+ parent.Entries.append(entry)
+
+ xml_parent = self.__getXmlMenu(parent.getPath(True, True))
+
+ if isinstance(entry, MenuEntry):
+ parent.MenuEntries.append(entry)
+ entry.Parents.append(parent)
+ self.__addXmlFilename(xml_parent, entry.DesktopFileID, "Include")
+ elif isinstance(entry, Menu):
+ parent.addSubmenu(entry)
+
+ if after or before:
+ self.__addLayout(parent)
+ self.__addXmlLayout(xml_parent, parent.Layout)
+
+ def __deleteEntry(self, parent, entry, after=None, before=None):
+ parent.Entries.remove(entry)
+
+ xml_parent = self.__getXmlMenu(parent.getPath(True, True))
+
+ if isinstance(entry, MenuEntry):
+ entry.Parents.remove(parent)
+ parent.MenuEntries.remove(entry)
+ self.__addXmlFilename(xml_parent, entry.DesktopFileID, "Exclude")
+ elif isinstance(entry, Menu):
+ parent.Submenus.remove(entry)
+
+ if after or before:
+ self.__addLayout(parent)
+ self.__addXmlLayout(xml_parent, parent.Layout)
+
+ def __deleteFile(self, filename):
+ try:
+ os.remove(filename)
+ except OSError:
+ pass
+ try:
+ self.filenames.remove(filename)
+ except ValueError:
+ pass
+
+ def __remove_whilespace_nodes(self, node):
+ remove_list = []
+ for child in node.childNodes:
+ if child.nodeType == xml.dom.minidom.Node.TEXT_NODE:
+ child.data = child.data.strip()
+ if not child.data.strip():
+ remove_list.append(child)
+ elif child.hasChildNodes():
+ self.__remove_whilespace_nodes(child)
+ for node in remove_list:
+ node.parentNode.removeChild(node)