move drlaunch in drlaunch
[drlaunch] / drlaunch / src / xdg / MenuEditor.py
diff --git a/drlaunch/src/xdg/MenuEditor.py b/drlaunch/src/xdg/MenuEditor.py
new file mode 100644 (file)
index 0000000..cc5ce54
--- /dev/null
@@ -0,0 +1,511 @@
+""" 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)