diff options
author | Thomas Kluyver <takowl@gmail.com> | 2013-05-25 08:32:21 -0700 |
---|---|---|
committer | Thomas Kluyver <takowl@gmail.com> | 2013-05-25 08:32:21 -0700 |
commit | 1ad37f33877f7f081e695d01d180c826c2589144 (patch) | |
tree | 49a9b9a23bca78ae8aadbc5f8d7c510bea4803e8 | |
parent | daca4a71c571e0aa273c4dc9e2600266b486b995 (diff) | |
parent | 7fd972e2383a504773203104e30909db77a1298f (diff) |
Merge pull request #4 from ju1ius/etree
Port of Menu & Menu editor to ElementTree
-rw-r--r-- | test/test-menu-rules.py | 10 | ||||
-rw-r--r-- | xdg/Menu.py | 1431 | ||||
-rw-r--r-- | xdg/MenuEditor.py | 256 |
3 files changed, 849 insertions, 848 deletions
diff --git a/test/test-menu-rules.py b/test/test-menu-rules.py index b5fee3b..1bfafb1 100644 --- a/test/test-menu-rules.py +++ b/test/test-menu-rules.py @@ -1,8 +1,7 @@ -import xml.dom.minidom +import xml.etree.cElementTree as etree import unittest -from xdg.Menu import Rule -from xdg.Menu import __parseRule as parseRule +from xdg.Menu import Parser, Rule _tests = [ @@ -161,9 +160,10 @@ class RulesTest(unittest.TestCase): """Basic rule matching tests""" def test_rule_from_node(self): + parser = Parser(debug=True) for i, test in enumerate(_tests): - root = xml.dom.minidom.parseString(test['doc']).childNodes[0] - rule = parseRule(root) + root = etree.fromstring(test['doc']) + rule = parser.parse_rule(root) for j, data in enumerate(test['data']): menuentry = MockMenuEntry(data[0], data[1]) result = eval(rule.code) diff --git a/xdg/Menu.py b/xdg/Menu.py index 2734554..ba5fe97 100644 --- a/xdg/Menu.py +++ b/xdg/Menu.py @@ -17,35 +17,38 @@ def print_menu(menu, tab=0): print_menu(parse()) """ -import locale, os, xml.dom.minidom +import os +import locale import subprocess import ast +try: + import xml.etree.cElementTree as etree +except ImportError: + import xml.etree.ElementTree as etree from xdg.BaseDirectory import xdg_data_dirs, xdg_config_dirs from xdg.DesktopEntry import DesktopEntry -from xdg.Exceptions import ParsingError, ValidationError, debug +from xdg.Exceptions import ParsingError, debug from xdg.util import PY3 import xdg.Locale import xdg.Config -ELEMENT_NODE = xml.dom.Node.ELEMENT_NODE -TEXT_NODE = xml.dom.Node.TEXT_NODE -CDATA_SECTION_NODE = xml.dom.Node.CDATA_SECTION_NODE def _strxfrm(s): """Wrapper around locale.strxfrm that accepts unicode strings on Python 2. - + See Python bug #2481. """ if (not PY3) and isinstance(s, unicode): s = s.encode('utf-8') return locale.strxfrm(s) + class Menu: """Menu containing sub menus under menu.Entries - Contains both Menu and MenuEntry items. + Contains both Menu and MenuEntry items. """ def __init__(self): # Public stuff @@ -114,11 +117,11 @@ class Menu: # FIXME: Performance: cache getName() def __cmp__(self, other): return locale.strcoll(self.getName(), other.getName()) - + def _key(self): """Key function for locale-aware sorting.""" return _strxfrm(self.getName()) - + def __lt__(self, other): try: other = other._key() @@ -136,21 +139,21 @@ class Menu: def getEntries(self, hidden=False): """Interator for a list of Entries visible to the user.""" for entry in self.Entries: - if hidden == True: + if hidden: yield entry - elif entry.Show == True: + elif entry.Show: yield entry # FIXME: Add searchEntry/seaqrchMenu function # search for name/comment/genericname/desktopfileide # return multiple items - def getMenuEntry(self, desktopfileid, deep = False): + def getMenuEntry(self, desktopfileid, deep=False): """Searches for a MenuEntry with a given DesktopFileID.""" for menuentry in self.MenuEntries: if menuentry.DesktopFileID == desktopfileid: return menuentry - if deep == True: + if deep: for submenu in self.Submenus: submenu.getMenuEntry(desktopfileid, deep) @@ -167,7 +170,7 @@ class Menu: def getPath(self, org=False, toplevel=False): """Returns this menu's path in the menu structure.""" parent = self - names=[] + names = [] while 1: if org: names.append(parent.Name) @@ -179,7 +182,7 @@ class Menu: break names.reverse() path = "" - if toplevel == False: + if not toplevel: names.pop(0) for name in names: path = os.path.join(path, name) @@ -213,6 +216,107 @@ class Menu: except AttributeError: return "" + def sort(self): + self.Entries = [] + self.Visible = 0 + + for submenu in self.Submenus: + submenu.sort() + + _submenus = set() + _entries = set() + + for order in self.Layout.order: + if order[0] == "Filename": + _entries.add(order[1]) + elif order[0] == "Menuname": + _submenus.add(order[1]) + + for order in self.Layout.order: + if order[0] == "Separator": + separator = Separator(self) + if len(self.Entries) > 0 and isinstance(self.Entries[-1], Separator): + separator.Show = False + self.Entries.append(separator) + elif order[0] == "Filename": + menuentry = self.getMenuEntry(order[1]) + if menuentry: + self.Entries.append(menuentry) + elif order[0] == "Menuname": + submenu = self.getMenu(order[1]) + if submenu: + if submenu.Layout.inline: + self.merge_inline(submenu) + else: + self.Entries.append(submenu) + elif order[0] == "Merge": + if order[1] == "files" or order[1] == "all": + self.MenuEntries.sort() + for menuentry in self.MenuEntries: + if menuentry.DesktopFileID not in _entries: + self.Entries.append(menuentry) + elif order[1] == "menus" or order[1] == "all": + self.Submenus.sort() + for submenu in self.Submenus: + if submenu.Name not in _submenus: + if submenu.Layout.inline: + self.merge_inline(submenu) + else: + self.Entries.append(submenu) + + # getHidden / NoDisplay / OnlyShowIn / NotOnlyShowIn / Deleted / NoExec + for entry in self.Entries: + entry.Show = True + self.Visible += 1 + if isinstance(entry, Menu): + if entry.Deleted is True: + entry.Show = "Deleted" + self.Visible -= 1 + elif isinstance(entry.Directory, MenuEntry): + if entry.Directory.DesktopEntry.getNoDisplay(): + entry.Show = "NoDisplay" + self.Visible -= 1 + elif entry.Directory.DesktopEntry.getHidden(): + entry.Show = "Hidden" + self.Visible -= 1 + elif isinstance(entry, MenuEntry): + if entry.DesktopEntry.getNoDisplay(): + entry.Show = "NoDisplay" + self.Visible -= 1 + elif entry.DesktopEntry.getHidden(): + entry.Show = "Hidden" + self.Visible -= 1 + elif entry.DesktopEntry.getTryExec() and not entry.DesktopEntry.findTryExec(): + entry.Show = "NoExec" + self.Visible -= 1 + elif xdg.Config.windowmanager: + if (entry.DesktopEntry.OnlyShowIn != [] and ( + xdg.Config.windowmanager not in entry.DesktopEntry.OnlyShowIn + ) + ) or ( + xdg.Config.windowmanager in entry.DesktopEntry.NotShowIn + ): + entry.Show = "NotShowIn" + self.Visible -= 1 + elif isinstance(entry, Separator): + self.Visible -= 1 + + # remove separators at the beginning and at the end + if len(self.Entries) > 0: + if isinstance(self.Entries[0], Separator): + self.Entries[0].Show = False + if len(self.Entries) > 1: + if isinstance(self.Entries[-1], Separator): + self.Entries[-1].Show = False + + # show_empty tag + for entry in self.Entries[:]: + if isinstance(entry, Menu) and not entry.Layout.show_empty and entry.Visible == 0: + entry.Show = "Empty" + self.Visible -= 1 + if entry.NotInXml is True: + self.Entries.remove(entry) + """ PRIVATE STUFF """ def addSubmenu(self, newmenu): for submenu in self.Submenus: @@ -224,95 +328,59 @@ class Menu: newmenu.Parent = self newmenu.Depth = self.Depth + 1 + # inline tags + def merge_inline(self, submenu): + """Appends a submenu's entries to this menu + See the <Menuname> section of the spec about the "inline" attribute + """ + if len(submenu.Entries) == 1 and submenu.Layout.inline_alias: + menuentry = submenu.Entries[0] + menuentry.DesktopEntry.set("Name", submenu.getName(), locale=True) + menuentry.DesktopEntry.set("GenericName", submenu.getGenericName(), locale=True) + menuentry.DesktopEntry.set("Comment", submenu.getComment(), locale=True) + self.Entries.append(menuentry) + elif len(submenu.Entries) <= submenu.Layout.inline_limit or submenu.Layout.inline_limit == 0: + if submenu.Layout.inline_header: + header = Header(submenu.getName(), submenu.getGenericName(), submenu.getComment()) + self.Entries.append(header) + for entry in submenu.Entries: + self.Entries.append(entry) + else: + self.Entries.append(submenu) + + class Move: "A move operation" - def __init__(self, node=None): - if node: - self.parseNode(node) - else: - self.Old = "" - self.New = "" + def __init__(self, old="", new=""): + self.Old = old + self.New = new def __cmp__(self, other): return cmp(self.Old, other.Old) - def parseNode(self, node): - for child in node.childNodes: - if child.nodeType == ELEMENT_NODE: - if child.tagName == "Old": - try: - self.parseOld(child.childNodes[0].nodeValue) - except IndexError: - raise ValidationError('Old cannot be empty', '??') - elif child.tagName == "New": - try: - self.parseNew(child.childNodes[0].nodeValue) - except IndexError: - raise ValidationError('New cannot be empty', '??') - - def parseOld(self, value): - self.Old = value - def parseNew(self, value): - self.New = value - class Layout: "Menu Layout class" - def __init__(self, node=None): - self.order = [] - if node: - self.show_empty = node.getAttribute("show_empty") or "false" - self.inline = node.getAttribute("inline") or "false" - self.inline_limit = node.getAttribute("inline_limit") or 4 - self.inline_header = node.getAttribute("inline_header") or "true" - self.inline_alias = node.getAttribute("inline_alias") or "false" - self.inline_limit = int(self.inline_limit) - self.parseNode(node) - else: - self.show_empty = "false" - self.inline = "false" - self.inline_limit = 4 - self.inline_header = "true" - self.inline_alias = "false" - self.order.append(["Merge", "menus"]) - self.order.append(["Merge", "files"]) - - def parseNode(self, node): - for child in node.childNodes: - if child.nodeType == ELEMENT_NODE: - if child.tagName == "Menuname": - try: - self.parseMenuname( - child.childNodes[0].nodeValue, - child.getAttribute("show_empty") or "false", - child.getAttribute("inline") or "false", - child.getAttribute("inline_limit") or 4, - child.getAttribute("inline_header") or "true", - child.getAttribute("inline_alias") or "false") - except IndexError: - raise ValidationError('Menuname cannot be empty', "") - elif child.tagName == "Separator": - self.parseSeparator() - elif child.tagName == "Filename": - try: - self.parseFilename(child.childNodes[0].nodeValue) - except IndexError: - raise ValidationError('Filename cannot be empty', "") - elif child.tagName == "Merge": - self.parseMerge(child.getAttribute("type") or "all") - - def parseMenuname(self, value, empty="false", inline="false", inline_limit=4, inline_header="true", inline_alias="false"): - self.order.append(["Menuname", value, empty, inline, inline_limit, inline_header, inline_alias]) - self.order[-1][4] = int(self.order[-1][4]) - - def parseSeparator(self): - self.order.append(["Separator"]) - - def parseFilename(self, value): - self.order.append(["Filename", value]) - - def parseMerge(self, type="all"): - self.order.append(["Merge", type]) + def __init__(self, show_empty=False, inline=False, inline_limit=4, + inline_header=True, inline_alias=False): + self.show_empty = show_empty + self.inline = inline + self.inline_limit = inline_limit + self.inline_header = inline_header + self.inline_alias = inline_alias + self._order = [] + self._default_order = [ + ['Merge', 'menus'], + ['Merge', 'files'] + ] + + @property + def order(self): + return self._order if self._order else self._default_order + + @order.setter + def order(self, order): + self._order = order class Rule: @@ -364,9 +432,14 @@ class Rule: class MenuEntry: "Wrapper for 'Menu Style' Desktop Entries" + + TYPE_USER = "User" + TYPE_SYSTEM = "System" + TYPE_BOTH = "Both" + def __init__(self, filename, dir="", prefix=""): # Create entry - self.DesktopEntry = DesktopEntry(os.path.join(dir,filename)) + self.DesktopEntry = DesktopEntry(os.path.join(dir, filename)) self.setAttributes(filename, dir, prefix) # Can be one of Deleted/Hidden/Empty/NotShowIn/NoExec or True @@ -386,7 +459,7 @@ class MenuEntry: def save(self): """Save any changes to the desktop entry.""" - if self.DesktopEntry.tainted == True: + if self.DesktopEntry.tainted: self.DesktopEntry.write() def getDir(self): @@ -395,20 +468,20 @@ class MenuEntry: def getType(self): """Return the type of MenuEntry, System/User/Both""" - if xdg.Config.root_mode == False: + if not xdg.Config.root_mode: if self.Original: - return "Both" + return self.TYPE_BOTH elif xdg_data_dirs[0] in self.DesktopEntry.filename: - return "User" + return self.TYPE_USER else: - return "System" + return self.TYPE_SYSTEM else: - return "User" + return self.TYPE_USER def setAttributes(self, filename, dir="", prefix=""): self.Filename = filename self.Prefix = prefix - self.DesktopFileID = os.path.join(prefix,filename).replace("/", "-") + self.DesktopFileID = os.path.join(prefix, filename).replace("/", "-") if not os.path.isabs(self.DesktopEntry.filename): self.__setFilename() @@ -419,32 +492,31 @@ class MenuEntry: self.__setFilename() def __setFilename(self): - if xdg.Config.root_mode == False: + if not xdg.Config.root_mode: path = xdg_data_dirs[0] else: - path= xdg_data_dirs[1] + path = xdg_data_dirs[1] if self.DesktopEntry.getType() == "Application": - dir = os.path.join(path, "applications") + dir_ = os.path.join(path, "applications") else: - dir = os.path.join(path, "desktop-directories") + dir_ = os.path.join(path, "desktop-directories") - self.DesktopEntry.filename = os.path.join(dir, self.Filename) + self.DesktopEntry.filename = os.path.join(dir_, self.Filename) def __cmp__(self, other): return locale.strcoll(self.DesktopEntry.getName(), other.DesktopEntry.getName()) - + def _key(self): """Key function for locale-aware sorting.""" return _strxfrm(self.DesktopEntry.getName()) - + def __lt__(self, other): try: other = other._key() except AttributeError: pass return self._key() < other - def __eq__(self, other): if self.DesktopFileID == str(other): @@ -473,629 +545,519 @@ class Header: def __str__(self): return self.Name -# Some XML utility functions - -def _get_children(node): - return [n for n in node.childNodes if n.nodeType == ELEMENT_NODE] +TYPE_DIR, TYPE_FILE = 0, 1 -def _iter_children(node): - for n in node.childNodes: - if n.nodeType == ELEMENT_NODE: - yield n +def _check_file_path(value, filename, type): + path = os.path.dirname(filename) + if not os.path.isabs(value): + value = os.path.join(path, value) + value = os.path.abspath(value) + if not os.path.exists(value): + return False + if type == TYPE_DIR and os.path.isdir(value): + return value + if type == TYPE_FILE and os.path.isfile(value): + return value + return False -def _get_node_text(node): - return ' '.join([ - n.nodeValue.strip() for n in node.childNodes if n.nodeType in (TEXT_NODE, CDATA_SECTION_NODE) - ]).strip() +def _get_menu_file_path(filename): + dirs = list(xdg_config_dirs) + if xdg.Config.root_mode is True: + dirs.pop(0) + for d in dirs: + menuname = os.path.join(d, "menus", filename) + if os.path.isfile(menuname): + return menuname -tmp = {} +def _to_bool(value): + if isinstance(value, bool): + return value + return value.lower() == "true" -def __getFileName(filename): - dirs = xdg_config_dirs[:] - if xdg.Config.root_mode == True: - dirs.pop(0) - for dir in dirs: - menuname = os.path.join (dir, "menus" , filename) - if os.path.isdir(dir) and os.path.isfile(menuname): - return menuname +# remove duplicate entries from a list +def _dedupe(_list): + _set = {} + _list.reverse() + _list = [_set.setdefault(e, e) for e in _list if e not in _set] + _list.reverse() + return _list + + +class Parser(object): + + def __init__(self, debug=False): + self.debug = debug + + def parse(self, filename=None): + """Load an applications.menu file. + + filename : str, optional + The default is ``$XDG_CONFIG_DIRS/menus/${XDG_MENU_PREFIX}applications.menu``. + """ + # convert to absolute path + if filename and not os.path.isabs(filename): + filename = _get_menu_file_path(filename) + # use default if no filename given + if not filename: + candidate = os.environ.get('XDG_MENU_PREFIX', '') + "applications.menu" + filename = _get_menu_file_path(candidate) + if not filename: + raise ParsingError('File not found', "/etc/xdg/menus/%s" % candidate) + # check if it is a .menu file + if not filename.endswith(".menu"): + raise ParsingError('Not a .menu file', filename) + # create xml parser + try: + tree = etree.parse(filename) + except: + raise ParsingError('Not a valid .menu file', filename) -def parse(filename=None): - """Load an applications.menu file. - - filename : str, optional - The default is ``$XDG_CONFIG_DIRS/menus/${XDG_MENU_PREFIX}applications.menu``. - """ - # convert to absolute path - if filename and not os.path.isabs(filename): - filename = __getFileName(filename) - - # use default if no filename given - if not filename: - candidate = os.environ.get('XDG_MENU_PREFIX', '') + "applications.menu" - filename = __getFileName(candidate) - - if not filename: - raise ParsingError('File not found', "/etc/xdg/menus/%s" % candidate) - - # check if it is a .menu file - if not os.path.splitext(filename)[1] == ".menu": - raise ParsingError('Not a .menu file', filename) - - # create xml parser - try: - doc = xml.dom.minidom.parse(filename) - except xml.parsers.expat.ExpatError: - raise ParsingError('Not a valid .menu file', filename) - - # parse menufile - tmp["Root"] = "" - tmp["mergeFiles"] = [] - tmp["DirectoryDirs"] = [] - tmp["cache"] = MenuEntryCache() - - __parse(doc, filename, tmp["Root"]) - __parsemove(tmp["Root"]) - __postparse(tmp["Root"]) - - tmp["Root"].Doc = doc - tmp["Root"].Filename = filename - - # generate the menu - __genmenuNotOnlyAllocated(tmp["Root"]) - __genmenuOnlyAllocated(tmp["Root"]) - - # and finally sort - sort(tmp["Root"]) - - return tmp["Root"] - - -def __parse(node, filename, parent=None): - for child in node.childNodes: - if child.nodeType == ELEMENT_NODE: - if child.tagName == 'Menu': - __parseMenu(child, filename, parent) - elif child.tagName == 'AppDir': - try: - __parseAppDir(child.childNodes[0].nodeValue, filename, parent) - except IndexError: - raise ValidationError('AppDir cannot be empty', filename) - elif child.tagName == 'DefaultAppDirs': - __parseDefaultAppDir(filename, parent) - elif child.tagName == 'DirectoryDir': - try: - __parseDirectoryDir(child.childNodes[0].nodeValue, filename, parent) - except IndexError: - raise ValidationError('DirectoryDir cannot be empty', filename) - elif child.tagName == 'DefaultDirectoryDirs': - __parseDefaultDirectoryDir(filename, parent) - elif child.tagName == 'Name' : - try: - parent.Name = child.childNodes[0].nodeValue - except IndexError: - raise ValidationError('Name cannot be empty', filename) - elif child.tagName == 'Directory' : - try: - parent.Directories.append(child.childNodes[0].nodeValue) - except IndexError: - raise ValidationError('Directory cannot be empty', filename) - elif child.tagName == 'OnlyUnallocated': + # parse menufile + self._merged_files = set() + self._directory_dirs = set() + self.cache = MenuEntryCache() + + menu = self.parse_menu(tree.getroot(), filename) + menu.tree = tree + menu.filename = filename + + self.handle_moves(menu) + self.post_parse(menu) + + # generate the menu + self.generate_not_only_allocated(menu) + self.generate_only_allocated(menu) + + # and finally sort + menu.sort() + + return menu + + def parse_menu(self, node, filename): + menu = Menu() + self.parse_node(node, filename, menu) + return menu + + def parse_node(self, node, filename, parent=None): + num_children = len(node) + for child in node: + tag, text = child.tag, child.text + text = text.strip() if text else None + if tag == 'Menu': + menu = self.parse_menu(child, filename) + parent.addSubmenu(menu) + elif tag == 'AppDir' and text: + self.parse_app_dir(text, filename, parent) + elif tag == 'DefaultAppDirs': + self.parse_default_app_dir(filename, parent) + elif tag == 'DirectoryDir' and text: + self.parse_directory_dir(text, filename, parent) + elif tag == 'DefaultDirectoryDirs': + self.parse_default_directory_dir(filename, parent) + elif tag == 'Name' and text: + parent.Name = text + elif tag == 'Directory' and text: + parent.Directories.append(text) + elif tag == 'OnlyUnallocated': parent.OnlyUnallocated = True - elif child.tagName == 'NotOnlyUnallocated': + elif tag == 'NotOnlyUnallocated': parent.OnlyUnallocated = False - elif child.tagName == 'Deleted': + elif tag == 'Deleted': parent.Deleted = True - elif child.tagName == 'NotDeleted': + elif tag == 'NotDeleted': parent.Deleted = False - elif child.tagName == 'Include' or child.tagName == 'Exclude': - parent.Rules.append(__parseRule(child)) - elif child.tagName == 'MergeFile': - try: - if child.getAttribute("type") == "parent": - __parseMergeFile("applications.menu", child, filename, parent) - else: - __parseMergeFile(child.childNodes[0].nodeValue, child, filename, parent) - except IndexError: - raise ValidationError('MergeFile cannot be empty', filename) - elif child.tagName == 'MergeDir': - try: - __parseMergeDir(child.childNodes[0].nodeValue, child, filename, parent) - except IndexError: - raise ValidationError('MergeDir cannot be empty', filename) - elif child.tagName == 'DefaultMergeDirs': - __parseDefaultMergeDirs(child, filename, parent) - elif child.tagName == 'Move': - parent.Moves.append(Move(child)) - elif child.tagName == 'Layout': - if len(child.childNodes) > 1: - parent.Layout = Layout(child) - elif child.tagName == 'DefaultLayout': - if len(child.childNodes) > 1: - parent.DefaultLayout = Layout(child) - elif child.tagName == 'LegacyDir': - try: - __parseLegacyDir(child.childNodes[0].nodeValue, child.getAttribute("prefix"), filename, parent) - except IndexError: - raise ValidationError('LegacyDir cannot be empty', filename) - elif child.tagName == 'KDELegacyDirs': - __parseKDELegacyDirs(filename, parent) - -def __parsemove(menu): - for submenu in menu.Submenus: - __parsemove(submenu) - - # parse move operations - for move in menu.Moves: - move_from_menu = menu.getMenu(move.Old) - if move_from_menu: - move_to_menu = menu.getMenu(move.New) - - menus = move.New.split("/") - oldparent = None - while len(menus) > 0: - if not oldparent: - oldparent = menu - newmenu = oldparent.getMenu(menus[0]) - if not newmenu: - newmenu = Menu() - newmenu.Name = menus[0] - if len(menus) > 1: - newmenu.NotInXml = True - oldparent.addSubmenu(newmenu) - oldparent = newmenu - menus.pop(0) - - newmenu += move_from_menu - move_from_menu.Parent.Submenus.remove(move_from_menu) - -def __postparse(menu): - # unallocated / deleted - if menu.Deleted == "notset": - menu.Deleted = False - if menu.OnlyUnallocated == "notset": - menu.OnlyUnallocated = False - - # Layout Tags - if not menu.Layout or not menu.DefaultLayout: - if menu.DefaultLayout: - menu.Layout = menu.DefaultLayout - elif menu.Layout: - if menu.Depth > 0: - menu.DefaultLayout = menu.Parent.DefaultLayout - else: - menu.DefaultLayout = Layout() + elif tag == 'Include' or tag == 'Exclude': + parent.Rules.append(self.parse_rule(child)) + elif tag == 'MergeFile': + if child.attrib.get("type", None) == "parent": + self.parse_merge_file("applications.menu", child, filename, parent) + elif text: + self.parse_merge_file(text, child, filename, parent) + elif tag == 'MergeDir' and text: + self.parse_merge_dir(text, child, filename, parent) + elif tag == 'DefaultMergeDirs': + self.parse_default_merge_dirs(child, filename, parent) + elif tag == 'Move': + parent.Moves.append(self.parse_move(child)) + elif tag == 'Layout': + if num_children > 1: + parent.Layout = self.parse_layout(child) + elif tag == 'DefaultLayout': + if num_children > 1: + parent.DefaultLayout = self.parse_layout(child) + elif tag == 'LegacyDir' and text: + self.parse_legacy_dir(text, child.attrib.get("prefix", ""), filename, parent) + elif tag == 'KDELegacyDirs': + self.parse_kde_legacy_dirs(filename, parent) + + def parse_layout(self, node): + layout = Layout( + show_empty=_to_bool(node.attrib.get("show_empty", False)), + inline=_to_bool(node.attrib.get("inline", False)), + inline_limit=int(node.attrib.get("inline_limit", 4)), + inline_header=_to_bool(node.attrib.get("inline_header", True)), + inline_alias=_to_bool(node.attrib.get("inline_alias", False)) + ) + for child in node: + tag, text = child.tag, child.text + text = text.strip() if text else None + if tag == "Menuname" and text: + layout.order.append([ + "Menuname", + text, + _to_bool(child.attrib.get("show_empty", False)), + _to_bool(child.attrib.get("inline", False)), + int(child.attrib.get("inline_limit", 4)), + _to_bool(child.attrib.get("inline_header", True)), + _to_bool(child.attrib.get("inline_alias", False)) + ]) + elif tag == "Separator": + layout.order.append(['Separator']) + elif tag == "Filename" and text: + layout.order.append(["Filename", text]) + elif tag == "Merge": + layout.order.append([ + "Merge", + child.attrib.get("type", "all") + ]) + return layout + + def parse_move(self, node): + old, new = "", "" + for child in node: + tag, text = child.tag, child.text + text = text.strip() if text else None + if tag == "Old" and text: + old = text + elif tag == "New" and text: + new = text + return Move(old, new) + + # ---------- <Rule> parsing + + def parse_rule(self, node): + type = Rule.TYPE_INCLUDE if node.tag == 'Include' else Rule.TYPE_EXCLUDE + tree = ast.Expression(lineno=1, col_offset=0) + expr = self.parse_bool_op(node, ast.Or()) + if expr: + tree.body = expr else: - if menu.Depth > 0: - menu.Layout = menu.Parent.DefaultLayout - menu.DefaultLayout = menu.Parent.DefaultLayout - else: - menu.Layout = Layout() - menu.DefaultLayout = Layout() - - # add parent's app/directory dirs - if menu.Depth > 0: - menu.AppDirs = menu.Parent.AppDirs + menu.AppDirs - menu.DirectoryDirs = menu.Parent.DirectoryDirs + menu.DirectoryDirs - - # remove duplicates - menu.Directories = __removeDuplicates(menu.Directories) - menu.DirectoryDirs = __removeDuplicates(menu.DirectoryDirs) - menu.AppDirs = __removeDuplicates(menu.AppDirs) - - # go recursive through all menus - for submenu in menu.Submenus: - __postparse(submenu) - - # reverse so handling is easier - menu.Directories.reverse() - menu.DirectoryDirs.reverse() - menu.AppDirs.reverse() - - # get the valid .directory file out of the list - for directory in menu.Directories: - for dir in menu.DirectoryDirs: - if os.path.isfile(os.path.join(dir, directory)): - menuentry = MenuEntry(directory, dir) - if not menu.Directory: - menu.Directory = menuentry - elif menuentry.getType() == "System": - if menu.Directory.getType() == "User": - menu.Directory.Original = menuentry - if menu.Directory: - break + tree.body = ast.Name('False', ast.Load()) + ast.fix_missing_locations(tree) + return Rule(type, tree) + + def parse_bool_op(self, node, operator): + values = [] + for child in node: + rule = self.parse_rule_node(child) + if rule: + values.append(rule) + num_values = len(values) + if num_values > 1: + return ast.BoolOp(operator, values) + elif num_values == 1: + return values[0] + return None + + def parse_rule_node(self, node): + tag = node.tag + if tag == 'Or': + return self.parse_bool_op(node, ast.Or()) + elif tag == 'And': + return self.parse_bool_op(node, ast.And()) + elif tag == 'Not': + expr = self.parse_bool_op(node, ast.Or()) + return ast.UnaryOp(ast.Not(), expr) if expr else None + elif tag == 'All': + return ast.Name('True', ast.Load()) + elif tag == 'Category': + category = node.text + return ast.Compare( + left=ast.Str(category), + ops=[ast.In()], + comparators=[ast.Attribute( + value=ast.Name(id='menuentry', ctx=ast.Load()), + attr='Categories', + ctx=ast.Load() + )] + ) + elif tag == 'Filename': + filename = node.text + return ast.Compare( + left=ast.Str(filename), + ops=[ast.Eq()], + comparators=[ast.Attribute( + value=ast.Name(id='menuentry', ctx=ast.Load()), + attr='DesktopFileID', + ctx=ast.Load() + )] + ) + # ---------- App/Directory Dir Stuff -# Menu parsing stuff -def __parseMenu(child, filename, parent): - m = Menu() - __parse(child, filename, m) - if parent: - parent.addSubmenu(m) - else: - tmp["Root"] = m + def parse_app_dir(self, value, filename, parent): + value = _check_file_path(value, filename, TYPE_DIR) + if value: + parent.AppDirs.append(value) -# helper function -def __check(value, filename, type): - path = os.path.dirname(filename) + def parse_default_app_dir(self, filename, parent): + for d in reversed(xdg_data_dirs): + self.parse_app_dir(os.path.join(d, "applications"), filename, parent) - if not os.path.isabs(value): - value = os.path.join(path, value) + def parse_directory_dir(self, value, filename, parent): + value = _check_file_path(value, filename, TYPE_DIR) + if value: + parent.DirectoryDirs.append(value) + + def parse_default_directory_dir(self, filename, parent): + for d in reversed(xdg_data_dirs): + self.parse_directory_dir(os.path.join(d, "desktop-directories"), filename, parent) + + # ---------- Merge Stuff + + def parse_merge_file(self, value, child, filename, parent): + if child.attrib.get("type", None) == "parent": + for d in xdg_config_dirs: + rel_file = filename.replace(d, "").strip("/") + if rel_file != filename: + for p in xdg_config_dirs: + if d == p: + continue + if os.path.isfile(os.path.join(p, rel_file)): + self.merge_file(os.path.join(p, rel_file), child, parent) + break + else: + value = _check_file_path(value, filename, TYPE_FILE) + if value: + self.merge_file(value, child, parent) - value = os.path.abspath(value) + def parse_merge_dir(self, value, child, filename, parent): + value = _check_file_path(value, filename, TYPE_DIR) + if value: + for item in os.listdir(value): + try: + if item.endswith(".menu"): + self.merge_file(os.path.join(value, item), child, parent) + except UnicodeDecodeError: + continue - if type == "dir" and os.path.exists(value) and os.path.isdir(value): - return value - elif type == "file" and os.path.exists(value) and os.path.isfile(value): - return value - else: - return False + def parse_default_merge_dirs(self, child, filename, parent): + basename = os.path.splitext(os.path.basename(filename))[0] + for d in reversed(xdg_config_dirs): + self.parse_merge_dir(os.path.join(d, "menus", basename + "-merged"), child, filename, parent) -# App/Directory Dir Stuff -def __parseAppDir(value, filename, parent): - value = __check(value, filename, "dir") - if value: - parent.AppDirs.append(value) - -def __parseDefaultAppDir(filename, parent): - for dir in reversed(xdg_data_dirs): - __parseAppDir(os.path.join(dir, "applications"), filename, parent) - -def __parseDirectoryDir(value, filename, parent): - value = __check(value, filename, "dir") - if value: - parent.DirectoryDirs.append(value) - -def __parseDefaultDirectoryDir(filename, parent): - for dir in reversed(xdg_data_dirs): - __parseDirectoryDir(os.path.join(dir, "desktop-directories"), filename, parent) - -# Merge Stuff -def __parseMergeFile(value, child, filename, parent): - if child.getAttribute("type") == "parent": - for dir in xdg_config_dirs: - rel_file = filename.replace(dir, "").strip("/") - if rel_file != filename: - for p in xdg_config_dirs: - if dir == p: - continue - if os.path.isfile(os.path.join(p,rel_file)): - __mergeFile(os.path.join(p,rel_file),child,parent) - break - else: - value = __check(value, filename, "file") - if value: - __mergeFile(value, child, parent) - -def __parseMergeDir(value, child, filename, parent): - value = __check(value, filename, "dir") - if value: - for item in os.listdir(value): - try: - if os.path.splitext(item)[1] == ".menu": - __mergeFile(os.path.join(value, item), child, parent) - except UnicodeDecodeError: - continue + def merge_file(self, filename, child, parent): + # check for infinite loops + if filename in self._merged_files: + if debug: + raise ParsingError('Infinite MergeFile loop detected', filename) + else: + return + self._merged_files.add(filename) + # load file + try: + tree = etree.parse(filename) + except IOError: + if self.debug: + raise ParsingError('File not found', filename) + else: + return + except: + if self.debug: + raise ParsingError('Not a valid .menu file', filename) + else: + return + root = tree.getroot() + # append file + for child in root: + self.parse_node(child, filename, parent) + break -def __parseDefaultMergeDirs(child, filename, parent): - basename = os.path.splitext(os.path.basename(filename))[0] - for dir in reversed(xdg_config_dirs): - __parseMergeDir(os.path.join(dir, "menus", basename + "-merged"), child, filename, parent) + # ---------- Legacy Dir Stuff -def __mergeFile(filename, child, parent): - # check for infinite loops - if filename in tmp["mergeFiles"]: - if debug: - raise ParsingError('Infinite MergeFile loop detected', filename) - else: - return + def parse_legacy_dir(self, dir_, prefix, filename, parent): + m = self.merge_legacy_dir(dir_, prefix, filename, parent) + if m: + parent += m - tmp["mergeFiles"].append(filename) + def merge_legacy_dir(self, dir_, prefix, filename, parent): + dir_ = _check_file_path(dir_, filename, TYPE_DIR) + if dir_ and dir_ not in self._directory_dirs: + self._directory_dirs.add(dir_) + m = Menu() + m.AppDirs.append(dir_) + m.DirectoryDirs.append(dir_) + m.Name = os.path.basename(dir_) + m.NotInXml = True - # load file - try: - doc = xml.dom.minidom.parse(filename) - except IOError: - if debug: - raise ParsingError('File not found', filename) - else: - return - except xml.parsers.expat.ExpatError: - if debug: - raise ParsingError('Not a valid .menu file', filename) - else: - return + for item in os.listdir(dir_): + try: + if item == ".directory": + m.Directories.append(item) + elif os.path.isdir(os.path.join(dir_, item)): + m.addSubmenu(self.merge_legacy_dir( + os.path.join(dir_, item), + prefix, + filename, + parent + )) + except UnicodeDecodeError: + continue - # append file - for child in doc.childNodes: - if child.nodeType == ELEMENT_NODE: - __parse(child,filename,parent) - break + self.cache.add_menu_entries([dir_], prefix, True) + menuentries = self.cache.get_menu_entries([dir_], False) -# Legacy Dir Stuff -def __parseLegacyDir(dir, prefix, filename, parent): - m = __mergeLegacyDir(dir,prefix,filename,parent) - if m: - parent += m - -def __mergeLegacyDir(dir, prefix, filename, parent): - dir = __check(dir,filename,"dir") - if dir and dir not in tmp["DirectoryDirs"]: - tmp["DirectoryDirs"].append(dir) - - m = Menu() - m.AppDirs.append(dir) - m.DirectoryDirs.append(dir) - m.Name = os.path.basename(dir) - m.NotInXml = True - - for item in os.listdir(dir): - try: - if item == ".directory": - m.Directories.append(item) - elif os.path.isdir(os.path.join(dir,item)): - m.addSubmenu(__mergeLegacyDir(os.path.join(dir,item), prefix, filename, parent)) - except UnicodeDecodeError: - continue + for menuentry in menuentries: + categories = menuentry.Categories + if len(categories) == 0: + r = Rule.fromFilename(Rule.TYPE_INCLUDE, menuentry.DesktopFileId) + m.rules.append(r) + if not dir_ in parent.AppDirs: + categories.append("Legacy") + menuentry.Categories = categories - tmp["cache"].addMenuEntries([dir],prefix, True) - menuentries = tmp["cache"].getMenuEntries([dir], False) + return m - for menuentry in menuentries: - categories = menuentry.Categories - if len(categories) == 0: - r = Rule.fromFilename(Rule.TYPE_INCLUDE, menuentry.DesktopFileID) - m.Rules.append(r) - if not dir in parent.AppDirs: - categories.append("Legacy") - menuentry.Categories = categories - - return m - -def __parseKDELegacyDirs(filename, parent): - try: - proc = subprocess.Popen(['kde-config', '--path', 'apps'], - stdout=subprocess.PIPE, universal_newlines=True) - output = proc.communicate()[0].splitlines() - except OSError: - # If kde-config doesn't exist, ignore this. - return - - try: - for dir in output[0].split(":"): - __parseLegacyDir(dir,"kde", filename, parent) - except IndexError: - pass - -# ---------- <Rule> parsing - -def __parseRule(node): - type = Rule.TYPE_INCLUDE if node.tagName == 'Include' else Rule.TYPE_EXCLUDE - tree = ast.Expression(lineno=1, col_offset=0) - expr = __parseBoolOp(node, ast.Or()) - if expr: - tree.body = expr - else: - tree.body = ast.Name('False', ast.Load()) - ast.fix_missing_locations(tree) - return Rule(type, tree) - -def __parseBoolOp(node, operator): - values = [] - for child in _iter_children(node): - rule = __parseRuleNode(child) - if rule: - values.append(rule) - num_values = len(values) - if num_values > 1: - return ast.BoolOp(operator, values) - elif num_values == 1: - return values[0] - return None - -def __parseRuleNode(node): - tag = node.tagName - if tag == 'Or': - return __parseBoolOp(node, ast.Or()) - elif tag == 'And': - return __parseBoolOp(node, ast.And()) - elif tag == 'Not': - expr = __parseBoolOp(node, ast.Or()) - return ast.UnaryOp(ast.Not(), expr) if expr else None - elif tag == 'All': - return ast.Name('True', ast.Load()) - elif tag == 'Category': - category = _get_node_text(node) - return ast.Compare( - left=ast.Str(category), - ops=[ast.In()], - comparators=[ast.Attribute( - value=ast.Name(id='menuentry', ctx=ast.Load()), - attr='Categories', - ctx=ast.Load() - )] - ) - elif tag == 'Filename': - filename = _get_node_text(node) - return ast.Compare( - left=ast.Str(filename), - ops=[ast.Eq()], - comparators=[ast.Attribute( - value=ast.Name(id='menuentry', ctx=ast.Load()), - attr='DesktopFileID', - ctx=ast.Load() - )] - ) + def parse_kde_legacy_dirs(self, filename, parent): + try: + proc = subprocess.Popen( + ['kde-config', '--path', 'apps'], + stdout=subprocess.PIPE, + universal_newlines=True + ) + output = proc.communicate()[0].splitlines() + except OSError: + # If kde-config doesn't exist, ignore this. + return + try: + for dir_ in output[0].split(":"): + self.parse_legacy_dir(dir_, "kde", filename, parent) + except IndexError: + pass -# remove duplicate entries from a list -def __removeDuplicates(list): - set = {} - list.reverse() - list = [set.setdefault(e,e) for e in list if e not in set] - list.reverse() - return list - -# Finally generate the menu -def __genmenuNotOnlyAllocated(menu): - for submenu in menu.Submenus: - __genmenuNotOnlyAllocated(submenu) - - if menu.OnlyUnallocated == False: - tmp["cache"].addMenuEntries(menu.AppDirs) - menuentries = [] - for rule in menu.Rules: - menuentries = rule.apply(tmp["cache"].getMenuEntries(menu.AppDirs), 1) - for menuentry in menuentries: - if menuentry.Add == True: - menuentry.Parents.append(menu) - menuentry.Add = False - menuentry.Allocated = True - menu.MenuEntries.append(menuentry) - -def __genmenuOnlyAllocated(menu): - for submenu in menu.Submenus: - __genmenuOnlyAllocated(submenu) - - if menu.OnlyUnallocated == True: - tmp["cache"].addMenuEntries(menu.AppDirs) - menuentries = [] - for rule in menu.Rules: - menuentries = rule.apply(tmp["cache"].getMenuEntries(menu.AppDirs), 2) - for menuentry in menuentries: - if menuentry.Add == True: - menuentry.Parents.append(menu) - # menuentry.Add = False - # menuentry.Allocated = True - menu.MenuEntries.append(menuentry) - -# And sorting ... -def sort(menu): - menu.Entries = [] - menu.Visible = 0 - - for submenu in menu.Submenus: - sort(submenu) - - tmp_s = [] - tmp_e = [] - - for order in menu.Layout.order: - if order[0] == "Filename": - tmp_e.append(order[1]) - elif order[0] == "Menuname": - tmp_s.append(order[1]) - - for order in menu.Layout.order: - if order[0] == "Separator": - separator = Separator(menu) - if len(menu.Entries) > 0 and isinstance(menu.Entries[-1], Separator): - separator.Show = False - menu.Entries.append(separator) - elif order[0] == "Filename": - menuentry = menu.getMenuEntry(order[1]) - if menuentry: - menu.Entries.append(menuentry) - elif order[0] == "Menuname": - submenu = menu.getMenu(order[1]) - if submenu: - __parse_inline(submenu, menu) - elif order[0] == "Merge": - if order[1] == "files" or order[1] == "all": - menu.MenuEntries.sort() - for menuentry in menu.MenuEntries: - if menuentry not in tmp_e: - menu.Entries.append(menuentry) - elif order[1] == "menus" or order[1] == "all": - menu.Submenus.sort() - for submenu in menu.Submenus: - if submenu.Name not in tmp_s: - __parse_inline(submenu, menu) - - # getHidden / NoDisplay / OnlyShowIn / NotOnlyShowIn / Deleted / NoExec - for entry in menu.Entries: - entry.Show = True - menu.Visible += 1 - if isinstance(entry, Menu): - if entry.Deleted == True: - entry.Show = "Deleted" - menu.Visible -= 1 - elif isinstance(entry.Directory, MenuEntry): - if entry.Directory.DesktopEntry.getNoDisplay() == True: - entry.Show = "NoDisplay" - menu.Visible -= 1 - elif entry.Directory.DesktopEntry.getHidden() == True: - entry.Show = "Hidden" - menu.Visible -= 1 - elif isinstance(entry, MenuEntry): - if entry.DesktopEntry.getNoDisplay() == True: - entry.Show = "NoDisplay" - menu.Visible -= 1 - elif entry.DesktopEntry.getHidden() == True: - entry.Show = "Hidden" - menu.Visible -= 1 - elif entry.DesktopEntry.getTryExec() and not __try_exec(entry.DesktopEntry.getTryExec()): - entry.Show = "NoExec" - menu.Visible -= 1 - elif xdg.Config.windowmanager: - if ( entry.DesktopEntry.getOnlyShowIn() != [] and xdg.Config.windowmanager not in entry.DesktopEntry.getOnlyShowIn() ) \ - or xdg.Config.windowmanager in entry.DesktopEntry.getNotShowIn(): - entry.Show = "NotShowIn" - menu.Visible -= 1 - elif isinstance(entry,Separator): - menu.Visible -= 1 - - # remove separators at the beginning and at the end - if len(menu.Entries) > 0: - if isinstance(menu.Entries[0], Separator): - menu.Entries[0].Show = False - if len(menu.Entries) > 1: - if isinstance(menu.Entries[-1], Separator): - menu.Entries[-1].Show = False - - # show_empty tag - for entry in menu.Entries[:]: - if isinstance(entry, Menu) and entry.Layout.show_empty == "false" and entry.Visible == 0: - entry.Show = "Empty" - menu.Visible -= 1 - if entry.NotInXml == True: - menu.Entries.remove(entry) - -def __try_exec(executable): - paths = os.environ['PATH'].split(os.pathsep) - if not os.path.isfile(executable): - for p in paths: - f = os.path.join(p, executable) - if os.path.isfile(f): - if os.access(f, os.X_OK): - return True - else: - if os.access(executable, os.X_OK): - return True - return False + def post_parse(self, menu): + # unallocated / deleted + if menu.Deleted == "notset": + menu.Deleted = False + if menu.OnlyUnallocated == "notset": + menu.OnlyUnallocated = False + + # Layout Tags + if not menu.Layout or not menu.DefaultLayout: + if menu.DefaultLayout: + menu.Layout = menu.DefaultLayout + elif menu.Layout: + if menu.Depth > 0: + menu.DefaultLayout = menu.Parent.DefaultLayout + else: + menu.DefaultLayout = Layout() + else: + if menu.Depth > 0: + menu.Layout = menu.Parent.DefaultLayout + menu.DefaultLayout = menu.Parent.DefaultLayout + else: + menu.Layout = Layout() + menu.DefaultLayout = Layout() + + # add parent's app/directory dirs + if menu.Depth > 0: + menu.AppDirs = menu.Parent.AppDirs + menu.AppDirs + menu.DirectoryDirs = menu.Parent.DirectoryDirs + menu.DirectoryDirs + + # remove duplicates + menu.Directories = _dedupe(menu.Directories) + menu.DirectoryDirs = _dedupe(menu.DirectoryDirs) + menu.AppDirs = _dedupe(menu.AppDirs) + + # go recursive through all menus + for submenu in menu.Submenus: + self.post_parse(submenu) + + # reverse so handling is easier + menu.Directories.reverse() + menu.DirectoryDirs.reverse() + menu.AppDirs.reverse() + + # get the valid .directory file out of the list + for directory in menu.Directories: + for dir in menu.DirectoryDirs: + if os.path.isfile(os.path.join(dir, directory)): + menuentry = MenuEntry(directory, dir) + if not menu.Directory: + menu.Directory = menuentry + elif menuentry.Type == MenuEntry.TYPE_SYSTEM: + if menu.Directory.Type == MenuEntry.TYPE_USER: + menu.Directory.Original = menuentry + if menu.Directory: + break + + # Finally generate the menu + def generate_not_only_allocated(self, menu): + for submenu in menu.Submenus: + self.generate_not_only_allocated(submenu) + + if menu.OnlyUnallocated is False: + self.cache.add_menu_entries(menu.AppDirs) + menuentries = [] + for rule in menu.Rules: + menuentries = rule.apply(self.cache.get_menu_entries(menu.AppDirs), 1) + + for menuentry in menuentries: + if menuentry.Add is True: + menuentry.Parents.append(menu) + menuentry.Add = False + menuentry.Allocated = True + menu.MenuEntries.append(menuentry) + + def generate_only_allocated(self, menu): + for submenu in menu.Submenus: + self.generate_only_allocated(submenu) + + if menu.OnlyUnallocated is True: + self.cache.add_menu_entries(menu.AppDirs) + menuentries = [] + for rule in menu.Rules: + menuentries = rule.apply(self.cache.get_menu_entries(menu.AppDirs), 2) + for menuentry in menuentries: + if menuentry.Add is True: + menuentry.Parents.append(menu) + # menuentry.Add = False + # menuentry.Allocated = True + menu.MenuEntries.append(menuentry) + + def handle_moves(self, menu): + for submenu in menu.Submenus: + self.handle_moves(submenu) + # parse move operations + for move in menu.Moves: + move_from_menu = menu.getMenu(move.Old) + if move_from_menu: + # FIXME: this is assigned, but never used... + move_to_menu = menu.getMenu(move.New) + + menus = move.New.split("/") + oldparent = None + while len(menus) > 0: + if not oldparent: + oldparent = menu + newmenu = oldparent.getMenu(menus[0]) + if not newmenu: + newmenu = Menu() + newmenu.Name = menus[0] + if len(menus) > 1: + newmenu.NotInXml = True + oldparent.addSubmenu(newmenu) + oldparent = newmenu + menus.pop(0) + + newmenu += move_from_menu + move_from_menu.parent.Submenus.remove(move_from_menu) -# inline tags -def __parse_inline(submenu, menu): - if submenu.Layout.inline == "true": - if len(submenu.Entries) == 1 and submenu.Layout.inline_alias == "true": - menuentry = submenu.Entries[0] - menuentry.DesktopEntry.set("Name", submenu.getName(), locale = True) - menuentry.DesktopEntry.set("GenericName", submenu.getGenericName(), locale = True) - menuentry.DesktopEntry.set("Comment", submenu.getComment(), locale = True) - menu.Entries.append(menuentry) - elif len(submenu.Entries) <= submenu.Layout.inline_limit or submenu.Layout.inline_limit == 0: - if submenu.Layout.inline_header == "true": - header = Header(submenu.getName(), submenu.getGenericName(), submenu.getComment()) - menu.Entries.append(header) - for entry in submenu.Entries: - menu.Entries.append(entry) - else: - menu.Entries.append(submenu) - else: - menu.Entries.append(submenu) class MenuEntryCache: "Class to cache Desktop Entries" @@ -1104,32 +1066,32 @@ class MenuEntryCache: self.cacheEntries['legacy'] = [] self.cache = {} - def addMenuEntries(self, dirs, prefix="", legacy=False): - for dir in dirs: - if not dir in self.cacheEntries: - self.cacheEntries[dir] = [] - self.__addFiles(dir, "", prefix, legacy) + def add_menu_entries(self, dirs, prefix="", legacy=False): + for dir_ in dirs: + if not dir_ in self.cacheEntries: + self.cacheEntries[dir_] = [] + self.__addFiles(dir_, "", prefix, legacy) - def __addFiles(self, dir, subdir, prefix, legacy): - for item in os.listdir(os.path.join(dir,subdir)): - if os.path.splitext(item)[1] == ".desktop": + def __addFiles(self, dir_, subdir, prefix, legacy): + for item in os.listdir(os.path.join(dir_, subdir)): + if item.endswith(".desktop"): try: - menuentry = MenuEntry(os.path.join(subdir,item), dir, prefix) + menuentry = MenuEntry(os.path.join(subdir, item), dir_, prefix) except ParsingError: continue - self.cacheEntries[dir].append(menuentry) - if legacy == True: + self.cacheEntries[dir_].append(menuentry) + if legacy: self.cacheEntries['legacy'].append(menuentry) - elif os.path.isdir(os.path.join(dir,subdir,item)) and legacy == False: - self.__addFiles(dir, os.path.join(subdir,item), prefix, legacy) + elif os.path.isdir(os.path.join(dir_, subdir, item)) and not legacy: + self.__addFiles(dir_, os.path.join(subdir, item), prefix, legacy) - def getMenuEntries(self, dirs, legacy=True): - list = [] - ids = [] + def get_menu_entries(self, dirs, legacy=True): + entries = [] + ids = set() # handle legacy items appdirs = dirs[:] - if legacy == True: + if legacy: appdirs.append("legacy") # cache the results again key = "".join(appdirs) @@ -1137,19 +1099,26 @@ class MenuEntryCache: return self.cache[key] except KeyError: pass - for dir in appdirs: - for menuentry in self.cacheEntries[dir]: + for dir_ in appdirs: + for menuentry in self.cacheEntries[dir_]: try: if menuentry.DesktopFileID not in ids: - ids.append(menuentry.DesktopFileID) - list.append(menuentry) - elif menuentry.getType() == "System": - # FIXME: This is only 99% correct, but still... - i = list.index(menuentry) - e = list[i] - if e.getType() == "User": - e.Original = menuentry + ids.add(menuentry.DesktopFileID) + entries.append(menuentry) + elif menuentry.getType() == MenuEntry.TYPE_SYSTEM: + # FIXME: This is only 99% correct, but still... + idx = entries.index(menuentry) + entry = entries[idx] + if entry.getType() == MenuEntry.TYPE_USER: + entry.Original = menuentry except UnicodeDecodeError: continue - self.cache[key] = list - return list + self.cache[key] = entries + return entries + + +def parse(filename=None): + """Helper function. + Equivalent to calling xdg.Menu.Parser().parse(filename) + """ + return Parser().parse(filename) diff --git a/xdg/MenuEditor.py b/xdg/MenuEditor.py index cc5ce54..0324f40 100644 --- a/xdg/MenuEditor.py +++ b/xdg/MenuEditor.py @@ -1,15 +1,17 @@ """ CLass to edit XDG Menus """ +import os +try: + import xml.etree.cElementTree as etree +except ImportError: + import xml.etree.ElementTree as etree +#FIXME avoid importing all from all modules 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 @@ -20,28 +22,31 @@ import re # FIXME: Advanced MenuEditing Stuff: LegacyDir/MergeFile # Complex Rules/Deleted/OnlyAllocated/AppDirs/DirectoryDirs -class MenuEditor: + +class MenuEditor(object): + def __init__(self, menu=None, filename=None, root=False): self.menu = None self.filename = None - self.doc = None + self.tree = None + self.parser = Parser() 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: + if root: setRootMode(True) if isinstance(menu, Menu): self.menu = menu elif menu: - self.menu = parse(menu) + self.menu = self.parser.parse(menu) else: - self.menu = parse() + self.menu = self.parser.parse() - if root == True: + if root: self.filename = self.menu.Filename elif filename: self.filename = filename @@ -49,13 +54,21 @@ class MenuEditor: 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) + self.tree = etree.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: + root = etree.fromtring(""" +<!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">%s</MergeFile> + </Menu> +""" % self.menu.Filename) + self.tree = etree.ElementTree(root) + except ParseError: raise ParsingError('Not a valid .menu file', self.filename) - self.__remove_whilespace_nodes(self.doc) + #FIXME: is this needed with etree ? + self.__remove_whitespace_nodes(self.tree) def save(self): self.__saveEntries(self.menu) @@ -67,7 +80,7 @@ class MenuEditor: self.__addEntry(parent, menuentry, after, before) - sort(self.menu) + self.menu.sort() return menuentry @@ -83,7 +96,7 @@ class MenuEditor: self.__addEntry(parent, menu, after, before) - sort(self.menu) + self.menu.sort() return menu @@ -92,7 +105,7 @@ class MenuEditor: self.__addEntry(parent, separator, after, before) - sort(self.menu) + self.menu.sort() return separator @@ -100,7 +113,7 @@ class MenuEditor: self.__deleteEntry(oldparent, menuentry, after, before) self.__addEntry(newparent, menuentry, after, before) - sort(self.menu) + self.menu.sort() return menuentry @@ -112,7 +125,7 @@ class MenuEditor: 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) + self.menu.sort() return menu @@ -120,14 +133,14 @@ class MenuEditor: self.__deleteEntry(parent, separator, after, before) self.__addEntry(parent, separator, after, before) - sort(self.menu) + self.menu.sort() return separator def copyMenuEntry(self, menuentry, oldparent, newparent, after=None, before=None): self.__addEntry(newparent, menuentry, after, before) - sort(self.menu) + self.menu.sort() return menuentry @@ -137,39 +150,39 @@ class MenuEditor: if name: if not deskentry.hasKey("Name"): deskentry.set("Name", name) - deskentry.set("Name", name, locale = True) + deskentry.set("Name", name, locale=True) if comment: if not deskentry.hasKey("Comment"): deskentry.set("Comment", comment) - deskentry.set("Comment", comment, locale = True) + deskentry.set("Comment", comment, locale=True) if genericname: - if not deskentry.hasKey("GnericNe"): + if not deskentry.hasKey("GenericName"): deskentry.set("GenericName", genericname) - deskentry.set("GenericName", genericname, locale = True) + deskentry.set("GenericName", genericname, locale=True) if command: deskentry.set("Exec", command) if icon: deskentry.set("Icon", icon) - if terminal == True: + if terminal: deskentry.set("Terminal", "true") - elif terminal == False: + elif not terminal: deskentry.set("Terminal", "false") - if nodisplay == True: + if nodisplay is True: deskentry.set("NoDisplay", "true") - elif nodisplay == False: + elif nodisplay is False: deskentry.set("NoDisplay", "false") - if hidden == True: + if hidden is True: deskentry.set("Hidden", "true") - elif hidden == False: + elif hidden is False: deskentry.set("Hidden", "false") menuentry.updateAttributes() if len(menuentry.Parents) > 0: - sort(self.menu) + self.menu.sort() return menuentry @@ -195,56 +208,58 @@ class MenuEditor: if name: if not deskentry.hasKey("Name"): deskentry.set("Name", name) - deskentry.set("Name", name, locale = True) + deskentry.set("Name", name, locale=True) if genericname: if not deskentry.hasKey("GenericName"): deskentry.set("GenericName", genericname) - deskentry.set("GenericName", genericname, locale = True) + deskentry.set("GenericName", genericname, locale=True) if comment: if not deskentry.hasKey("Comment"): deskentry.set("Comment", comment) - deskentry.set("Comment", comment, locale = True) + deskentry.set("Comment", comment, locale=True) if icon: deskentry.set("Icon", icon) - if nodisplay == True: + if nodisplay is True: deskentry.set("NoDisplay", "true") - elif nodisplay == False: + elif nodisplay is False: deskentry.set("NoDisplay", "false") - if hidden == True: + if hidden is True: deskentry.set("Hidden", "true") - elif hidden == False: + elif hidden is False: deskentry.set("Hidden", "false") menu.Directory.updateAttributes() if isinstance(menu.Parent, Menu): - sort(self.menu) + self.menu.sort() return menu def hideMenuEntry(self, menuentry): - self.editMenuEntry(menuentry, nodisplay = True) + self.editMenuEntry(menuentry, nodisplay=True) def unhideMenuEntry(self, menuentry): - self.editMenuEntry(menuentry, nodisplay = False, hidden = False) + self.editMenuEntry(menuentry, nodisplay=False, hidden=False) def hideMenu(self, menu): - self.editMenu(menu, nodisplay = True) + 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) + self.editMenu(menu, nodisplay=False, hidden=False) + xml_menu = self.__getXmlMenu(menu.getPath(True, True), False) + deleted = xml_menu.findall('Deleted') + not_deleted = xml_menu.findall('NotDeleted') + for node in deleted + not_deleted: + xml_menu.remove(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) + self.menu.sort() return menuentry def revertMenuEntry(self, menuentry): @@ -257,7 +272,7 @@ class MenuEditor: index = parent.MenuEntries.index(menuentry) parent.MenuEntries[index] = menuentry.Original menuentry.Original.Parents.append(parent) - sort(self.menu) + self.menu.sort() return menuentry def deleteMenu(self, menu): @@ -265,21 +280,22 @@ class MenuEditor: 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) + parent = self.__get_parent_node(xml_menu) + parent.remove(xml_menu) + self.menu.sort() 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) + self.menu.sort() return menu def deleteSeparator(self, separator): self.__deleteEntry(separator.Parent, separator, after=True) - sort(self.menu) + self.menu.sort() return separator @@ -290,8 +306,9 @@ class MenuEditor: return "none" elif entry.Directory.getType() == "Both": return "revert" - elif entry.Directory.getType() == "User" \ - and (len(entry.Submenus) + len(entry.MenuEntries)) == 0: + elif entry.Directory.getType() == "User" and ( + len(entry.Submenus) + len(entry.MenuEntries) + ) == 0: return "delete" elif isinstance(entry, MenuEntry): @@ -318,9 +335,7 @@ class MenuEditor: 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() + self.tree.write(self.filename, encoding='utf-8') def __getFileName(self, name, extension): postfix = 0 @@ -333,8 +348,9 @@ class MenuEditor: 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)): + 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: @@ -343,8 +359,11 @@ class MenuEditor: return filename def __getXmlMenu(self, path, create=True, element=None): + # FIXME: we should also return the menu's parent, + # to avoid looking for it later on + # @see Element.getiterator() if not element: - element = self.doc + element = self.tree if "/" in path: (name, path) = path.split("/", 1) @@ -353,17 +372,16 @@ class MenuEditor: 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 + for node in element.findall("Menu"): + name_node = node.find('Name') + if name_node.text == name: + if path: + found = self.__getXmlMenu(path, create, node) + else: + found = node if found: break - if not found and create == True: + if not found and create: node = self.__addXmlMenuElement(element, name) if path: found = self.__getXmlMenu(path, create, node) @@ -373,58 +391,62 @@ class MenuEditor: return found def __addXmlMenuElement(self, element, name): - node = self.doc.createElement('Menu') - self.__addXmlTextElement(node, 'Name', name) - return element.appendChild(node) + menu_node = etree.SubElement('Menu', element) + name_node = etree.SubElement('Name', menu_node) + name_node.text = name + return menu_node def __addXmlTextElement(self, element, name, text): - node = self.doc.createElement(name) - text = self.doc.createTextNode(text) - node.appendChild(text) - return element.appendChild(node) + node = etree.SubElement(name, element) + node.text = text + return node - def __addXmlFilename(self, element, filename, type = "Include"): + 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) + includes = element.findall('Include') + excludes = element.findall('Exclude') + rules = includes + excludes + for rule in rules: + #FIXME: this finds only Rules whose FIRST child is a Filename element + if rule[0].tag == "Filename" and rule[0].text == filename: + element.remove(rule) + # shouldn't it remove all occurences, like the following: + #filename_nodes = rule.findall('.//Filename'): + #for fn in filename_nodes: + #if fn.text == filename: + ##element.remove(rule) + #parent = self.__get_parent_node(fn) + #parent.remove(fn) # add new filename - node = self.doc.createElement(type) - node.appendChild(self.__addXmlTextElement(node, 'Filename', filename)) - return element.appendChild(node) + node = etree.SubElement(type_, element) + self.__addXmlTextElement(node, 'Filename', filename) + return 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) + node = etree.SubElement("Move", element) + self.__addXmlTextElement(node, 'Old', old) + self.__addXmlTextElement(node, 'New', new) + return node def __addXmlLayout(self, element, layout): # remove old layout - for node in self.__getXmlNodesByName("Layout", element): - element.removeChild(node) + for node in element.findall("Layout"): + element.remove(node) # add new layout - node = self.doc.createElement("Layout") + node = etree.SubElement("Layout", element) for order in layout.order: if order[0] == "Separator": - child = self.doc.createElement("Separator") - node.appendChild(child) + child = etree.SubElement("Separator", node) 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 + child = etree.SubElement("Merge", node) + child.attrib["type"] = order[1] + return node def __addLayout(self, parent): layout = Layout() @@ -498,14 +520,24 @@ class MenuEditor: 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(): + def __remove_whitespace_nodes(self, node): + for child in node: + text = child.text.strip() + if not text: + child.text = '' + tail = child.tail.strip() + if not tail: + child.tail = '' + if len(child): self.__remove_whilespace_nodes(child) - for node in remove_list: - node.parentNode.removeChild(node) + + def __get_parent_node(self, node): + # elements in ElementTree doesn't hold a reference to their parent + for parent, child in self.__iter_parent(): + if child is node: + return child + + def __iter_parent(self): + for parent in self.tree.getiterator(): + for child in parent: + yield parent, child |