""" Implementation of the XDG Menu Specification http://standards.freedesktop.org/menu-spec/ Example code: from xdg.Menu import parse, Menu, MenuEntry def print_menu(menu, tab=0): for submenu in menu.Entries: if isinstance(submenu, Menu): print ("\t" * tab) + unicode(submenu) print_menu(submenu, tab+1) elif isinstance(submenu, MenuEntry): print ("\t" * tab) + unicode(submenu.DesktopEntry) print_menu(parse()) """ import os import locale import subprocess import ast try: import xml.etree.cElementTree as etree except ImportError: import xml.etree.ElementTree as etree from .BaseDirectory import xdg_data_dirs, xdg_config_dirs from . import DesktopEntry, Locale, Config from .Exceptions import ParsingError from .util import PY3 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) DELETED = "Deleted" NO_DISPLAY = "NoDisplay" HIDDEN = "Hidden" EMPTY = "Empty" NOT_SHOW_IN = "NotShowIn" NO_EXEC = "NoExec" class Menu: """Menu containing sub menus under menu.Entries Contains both Menu and MenuEntry items. """ def __init__(self): # Public stuff self.Name = "" self.Directory = None self.Entries = [] self.Doc = "" self.Filename = "" self.Depth = 0 self.Parent = None self.NotInXml = False # Can be True, False, DELETED, NO_DISPLAY, HIDDEN, EMPTY or NOT_SHOW_IN self.Show = True self.Visible = 0 # Private stuff, only needed for parsing self.AppDirs = [] self.DefaultLayout = None self.Deleted = None self.Directories = [] self.DirectoryDirs = [] self.Layout = None self.MenuEntries = [] self.Moves = [] self.OnlyUnallocated = None self.Rules = [] self.Submenus = [] def __str__(self): return self.Name def __add__(self, other): for dir in other.AppDirs: self.AppDirs.append(dir) for dir in other.DirectoryDirs: self.DirectoryDirs.append(dir) for directory in other.Directories: self.Directories.append(directory) if other.Deleted is not None: self.Deleted = other.Deleted if other.OnlyUnallocated is not None: self.OnlyUnallocated = other.OnlyUnallocated if other.Layout: self.Layout = other.Layout if other.DefaultLayout: self.DefaultLayout = other.DefaultLayout for rule in other.Rules: self.Rules.append(rule) for move in other.Moves: self.Moves.append(move) for submenu in other.Submenus: self.addSubmenu(submenu) return self # 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() except AttributeError: pass return self._key() < other def __eq__(self, other): try: return self.Name == unicode(other) except NameError: # unicode() becomes str() in Python 3 return self.Name == str(other) """ PUBLIC STUFF """ def getEntries(self, show_hidden=False): """Interator for a list of Entries visible to the user.""" for entry in self.Entries: if show_hidden: yield entry elif entry.Show is True: yield entry # FIXME: Add searchEntry/seaqrchMenu function # search for name/comment/genericname/desktopfileid # return multiple items 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: for submenu in self.Submenus: submenu.getMenuEntry(desktopfileid, deep) def getMenu(self, path): """Searches for a Menu with a given path.""" array = path.split("/", 1) for submenu in self.Submenus: if submenu.Name == array[0]: if len(array) > 1: return submenu.getMenu(array[1]) else: return submenu def getPath(self, org=False, toplevel=False): """Returns this menu's path in the menu structure.""" parent = self names = [] while 1: if org: names.append(parent.Name) else: names.append(parent.getName()) if parent.Depth > 0: parent = parent.Parent else: break names.reverse() path = "" if not toplevel: names.pop(0) for name in names: path = os.path.join(path, name) return path def getName(self): """Returns the menu's localised name.""" try: return self.Directory.DesktopEntry.getName() except AttributeError: return self.Name def getGenericName(self): """Returns the menu's generic name.""" try: return self.Directory.DesktopEntry.getGenericName() except AttributeError: return "" def getComment(self): """Returns the menu's comment text.""" try: return self.Directory.DesktopEntry.getComment() except AttributeError: return "" def getIcon(self): """Returns the menu's icon, filename or simple name""" try: return self.Directory.DesktopEntry.getIcon() 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 = NO_DISPLAY self.Visible -= 1 elif entry.Directory.DesktopEntry.getHidden(): entry.Show = HIDDEN self.Visible -= 1 elif isinstance(entry, MenuEntry): if entry.DesktopEntry.getNoDisplay(): entry.Show = NO_DISPLAY self.Visible -= 1 elif entry.DesktopEntry.getHidden(): entry.Show = HIDDEN self.Visible -= 1 elif entry.DesktopEntry.getTryExec() and not entry.DesktopEntry.findTryExec(): entry.Show = NO_EXEC 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 = NOT_SHOW_IN 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: if submenu == newmenu: submenu += newmenu break else: self.Submenus.append(newmenu) 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 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, old="", new=""): self.Old = old self.New = new def __cmp__(self, other): return cmp(self.Old, other.Old) class Layout: "Menu Layout class" 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: """Include / Exclude Rules Class""" TYPE_INCLUDE, TYPE_EXCLUDE = 0, 1 @classmethod def fromFilename(cls, type, filename): tree = ast.Expression( body=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() )] ), lineno=1, col_offset=0 ) ast.fix_missing_locations(tree) rule = Rule(type, tree) return rule def __init__(self, type, expression): # Type is TYPE_INCLUDE or TYPE_EXCLUDE self.Type = type # expression is ast.Expression self.expression = expression self.code = compile(self.expression, '', 'eval') def __str__(self): return ast.dump(self.expression) def apply(self, menuentries, run): for menuentry in menuentries: if run == 2 and (menuentry.MatchedInclude is True or menuentry.Allocated is True): continue if eval(self.code): if self.Type is Rule.TYPE_INCLUDE: menuentry.Add = True menuentry.MatchedInclude = True else: menuentry.Add = False return menuentries 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.setAttributes(filename, dir, prefix) # Can True, False DELETED, HIDDEN, EMPTY, NOT_SHOW_IN or NO_EXEC self.Show = True # Semi-Private self.Original = None self.Parents = [] # Private Stuff self.Allocated = False self.Add = False self.MatchedInclude = False # Caching self.Categories = self.DesktopEntry.getCategories() def save(self): """Save any changes to the desktop entry.""" if self.DesktopEntry.tainted: self.DesktopEntry.write() def getDir(self): """Return the directory containing the desktop entry file.""" return self.DesktopEntry.filename.replace(self.Filename, '') def getType(self): """Return the type of MenuEntry, System/User/Both""" if not xdg.Config.root_mode: if self.Original: return self.TYPE_BOTH elif xdg_data_dirs[0] in self.DesktopEntry.filename: return self.TYPE_USER else: return self.TYPE_SYSTEM else: return self.TYPE_USER def setAttributes(self, filename, dir="", prefix=""): self.Filename = filename self.Prefix = prefix self.DesktopFileID = os.path.join(prefix, filename).replace("/", "-") if not os.path.isabs(self.DesktopEntry.filename): self.__setFilename() def updateAttributes(self): if self.getType() == self.TYPE_SYSTEM: self.Original = MenuEntry(self.Filename, self.getDir(), self.Prefix) self.__setFilename() def __setFilename(self): if not xdg.Config.root_mode: path = xdg_data_dirs[0] else: path = xdg_data_dirs[1] if self.DesktopEntry.getType() == "Application": dir_ = os.path.join(path, "applications") else: dir_ = os.path.join(path, "desktop-directories") 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): return True else: return False def __repr__(self): return self.DesktopFileID class Separator: "Just a dummy class for Separators" def __init__(self, parent): self.Parent = parent self.Show = True class Header: "Class for Inline Headers" def __init__(self, name, generic_name, comment): self.Name = name self.GenericName = generic_name self.Comment = comment def __str__(self): return self.Name TYPE_DIR, TYPE_FILE = 0, 1 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_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 def _to_bool(value): if isinstance(value, bool): return value return value.lower() == "true" # 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 XMLMenuBuilder(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) # 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 tag == 'NotOnlyUnallocated': parent.OnlyUnallocated = False elif tag == 'Deleted': parent.Deleted = True elif tag == 'NotDeleted': parent.Deleted = False 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) # ---------- 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: 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 def parse_app_dir(self, value, filename, parent): value = _check_file_path(value, filename, TYPE_DIR) if value: parent.AppDirs.append(value) 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) 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) 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 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) def merge_file(self, filename, child, parent): # check for infinite loops if filename in self._merged_files: if self.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() self.parse_node(root, filename, parent) # ---------- Legacy Dir Stuff def parse_legacy_dir(self, dir_, prefix, filename, parent): m = self.merge_legacy_dir(dir_, prefix, filename, parent) if m: parent += m 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 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 self.cache.add_menu_entries([dir_], prefix, True) menuentries = self.cache.get_menu_entries([dir_], False) 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 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 def post_parse(self, menu): # unallocated / deleted if menu.Deleted is None: menu.Deleted = False if menu.OnlyUnallocated is None: 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) class MenuEntryCache: "Class to cache Desktop Entries" def __init__(self): self.cacheEntries = {} self.cacheEntries['legacy'] = [] self.cache = {} 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 item.endswith(".desktop"): try: menuentry = MenuEntry(os.path.join(subdir, item), dir_, prefix) except ParsingError: continue self.cacheEntries[dir_].append(menuentry) if legacy: self.cacheEntries['legacy'].append(menuentry) elif os.path.isdir(os.path.join(dir_, subdir, item)) and not legacy: self.__addFiles(dir_, os.path.join(subdir, item), prefix, legacy) def get_menu_entries(self, dirs, legacy=True): entries = [] ids = set() # handle legacy items appdirs = dirs[:] if legacy: appdirs.append("legacy") # cache the results again key = "".join(appdirs) try: return self.cache[key] except KeyError: pass for dir_ in appdirs: for menuentry in self.cacheEntries[dir_]: try: if menuentry.DesktopFileID not in ids: 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] = entries return entries def parse(filename=None, debug=False): """Helper function. Equivalent to calling xdg.Menu.XMLMenuBuilder().parse(filename) """ return XMLMenuBuilder(debug).parse(filename)