1"""
2Implementation of the XDG Menu Specification Version 1.0.draft-1
3http://standards.freedesktop.org/menu-spec/
4
5Example code:
6
7from xdg.Menu import parse, Menu, MenuEntry
8
9def print_menu(menu, tab=0):
10  for submenu in menu.Entries:
11    if isinstance(submenu, Menu):
12      print ("\t" * tab) + unicode(submenu)
13      print_menu(submenu, tab+1)
14    elif isinstance(submenu, MenuEntry):
15      print ("\t" * tab) + unicode(submenu.DesktopEntry)
16
17print_menu(parse())
18"""
19
20import locale, os, xml.dom.minidom
21import subprocess
22
23from xdg.BaseDirectory import xdg_data_dirs, xdg_config_dirs
24from xdg.DesktopEntry import DesktopEntry
25from xdg.Exceptions import ParsingError, ValidationError, debug
26from xdg.util import PY3
27
28import xdg.Locale
29import xdg.Config
30
31ELEMENT_NODE = xml.dom.Node.ELEMENT_NODE
32
33def _strxfrm(s):
34    """Wrapper around locale.strxfrm that accepts unicode strings on Python 2.
35
36    See Python bug #2481.
37    """
38    if (not PY3) and isinstance(s, unicode):
39        s = s.encode('utf-8')
40    return locale.strxfrm(s)
41
42class Menu:
43    """Menu containing sub menus under menu.Entries
44
45	Contains both Menu and MenuEntry items.
46    """
47    def __init__(self):
48        # Public stuff
49        self.Name = ""
50        self.Directory = None
51        self.Entries = []
52        self.Doc = ""
53        self.Filename = ""
54        self.Depth = 0
55        self.Parent = None
56        self.NotInXml = False
57
58        # Can be one of Deleted/NoDisplay/Hidden/Empty/NotShowIn or True
59        self.Show = True
60        self.Visible = 0
61
62        # Private stuff, only needed for parsing
63        self.AppDirs = []
64        self.DefaultLayout = None
65        self.Deleted = "notset"
66        self.Directories = []
67        self.DirectoryDirs = []
68        self.Layout = None
69        self.MenuEntries = []
70        self.Moves = []
71        self.OnlyUnallocated = "notset"
72        self.Rules = []
73        self.Submenus = []
74
75    def __str__(self):
76        return self.Name
77
78    def __add__(self, other):
79        for dir in other.AppDirs:
80            self.AppDirs.append(dir)
81
82        for dir in other.DirectoryDirs:
83            self.DirectoryDirs.append(dir)
84
85        for directory in other.Directories:
86            self.Directories.append(directory)
87
88        if other.Deleted != "notset":
89            self.Deleted = other.Deleted
90
91        if other.OnlyUnallocated != "notset":
92            self.OnlyUnallocated = other.OnlyUnallocated
93
94        if other.Layout:
95            self.Layout = other.Layout
96
97        if other.DefaultLayout:
98            self.DefaultLayout = other.DefaultLayout
99
100        for rule in other.Rules:
101            self.Rules.append(rule)
102
103        for move in other.Moves:
104            self.Moves.append(move)
105
106        for submenu in other.Submenus:
107            self.addSubmenu(submenu)
108
109        return self
110
111    # FIXME: Performance: cache getName()
112    def __cmp__(self, other):
113        return locale.strcoll(self.getName(), other.getName())
114
115    def _key(self):
116        """Key function for locale-aware sorting."""
117        return _strxfrm(self.getName())
118
119    def __lt__(self, other):
120        try:
121            other = other._key()
122        except AttributeError:
123            pass
124        return self._key() < other
125
126    def __eq__(self, other):
127        try:
128            return self.Name == unicode(other)
129        except NameError:  # unicode() becomes str() in Python 3
130            return self.Name == str(other)
131
132    """ PUBLIC STUFF """
133    def getEntries(self, hidden=False):
134        """Interator for a list of Entries visible to the user."""
135        for entry in self.Entries:
136            if hidden == True:
137                yield entry
138            elif entry.Show == True:
139                yield entry
140
141    # FIXME: Add searchEntry/seaqrchMenu function
142    # search for name/comment/genericname/desktopfileide
143    # return multiple items
144
145    def getMenuEntry(self, desktopfileid, deep = False):
146        """Searches for a MenuEntry with a given DesktopFileID."""
147        for menuentry in self.MenuEntries:
148            if menuentry.DesktopFileID == desktopfileid:
149                return menuentry
150        if deep == True:
151            for submenu in self.Submenus:
152                submenu.getMenuEntry(desktopfileid, deep)
153
154    def getMenu(self, path):
155        """Searches for a Menu with a given path."""
156        array = path.split("/", 1)
157        for submenu in self.Submenus:
158            if submenu.Name == array[0]:
159                if len(array) > 1:
160                    return submenu.getMenu(array[1])
161                else:
162                    return submenu
163
164    def getPath(self, org=False, toplevel=False):
165        """Returns this menu's path in the menu structure."""
166        parent = self
167        names=[]
168        while 1:
169            if org:
170                names.append(parent.Name)
171            else:
172                names.append(parent.getName())
173            if parent.Depth > 0:
174                parent = parent.Parent
175            else:
176                break
177        names.reverse()
178        path = ""
179        if toplevel == False:
180            names.pop(0)
181        for name in names:
182            path = os.path.join(path, name)
183        return path
184
185    def getName(self):
186        """Returns the menu's localised name."""
187        try:
188            return self.Directory.DesktopEntry.getName()
189        except AttributeError:
190            return self.Name
191
192    def getGenericName(self):
193        """Returns the menu's generic name."""
194        try:
195            return self.Directory.DesktopEntry.getGenericName()
196        except AttributeError:
197            return ""
198
199    def getComment(self):
200        """Returns the menu's comment text."""
201        try:
202            return self.Directory.DesktopEntry.getComment()
203        except AttributeError:
204            return ""
205
206    def getIcon(self):
207        """Returns the menu's icon, filename or simple name"""
208        try:
209            return self.Directory.DesktopEntry.getIcon()
210        except AttributeError:
211            return ""
212
213    """ PRIVATE STUFF """
214    def addSubmenu(self, newmenu):
215        for submenu in self.Submenus:
216            if submenu == newmenu:
217                submenu += newmenu
218                break
219        else:
220            self.Submenus.append(newmenu)
221            newmenu.Parent = self
222            newmenu.Depth = self.Depth + 1
223
224class Move:
225    "A move operation"
226    def __init__(self, node=None):
227        if node:
228            self.parseNode(node)
229        else:
230            self.Old = ""
231            self.New = ""
232
233    def __cmp__(self, other):
234        return cmp(self.Old, other.Old)
235
236    def parseNode(self, node):
237        for child in node.childNodes:
238            if child.nodeType == ELEMENT_NODE:
239                if child.tagName == "Old":
240                    try:
241                        self.parseOld(child.childNodes[0].nodeValue)
242                    except IndexError:
243                        raise ValidationError('Old cannot be empty', '??')
244                elif child.tagName == "New":
245                    try:
246                        self.parseNew(child.childNodes[0].nodeValue)
247                    except IndexError:
248                        raise ValidationError('New cannot be empty', '??')
249
250    def parseOld(self, value):
251        self.Old = value
252    def parseNew(self, value):
253        self.New = value
254
255
256class Layout:
257    "Menu Layout class"
258    def __init__(self, node=None):
259        self.order = []
260        if node:
261            self.show_empty = node.getAttribute("show_empty") or "false"
262            self.inline = node.getAttribute("inline") or "false"
263            self.inline_limit = node.getAttribute("inline_limit") or 4
264            self.inline_header = node.getAttribute("inline_header") or "true"
265            self.inline_alias = node.getAttribute("inline_alias") or "false"
266            self.inline_limit = int(self.inline_limit)
267            self.parseNode(node)
268        else:
269            self.show_empty = "false"
270            self.inline = "false"
271            self.inline_limit = 4
272            self.inline_header = "true"
273            self.inline_alias = "false"
274            self.order.append(["Merge", "menus"])
275            self.order.append(["Merge", "files"])
276
277    def parseNode(self, node):
278        for child in node.childNodes:
279            if child.nodeType == ELEMENT_NODE:
280                if child.tagName == "Menuname":
281                    try:
282                        self.parseMenuname(
283                            child.childNodes[0].nodeValue,
284                            child.getAttribute("show_empty") or "false",
285                            child.getAttribute("inline") or "false",
286                            child.getAttribute("inline_limit") or 4,
287                            child.getAttribute("inline_header") or "true",
288                            child.getAttribute("inline_alias") or "false" )
289                    except IndexError:
290                        raise ValidationError('Menuname cannot be empty', "")
291                elif child.tagName == "Separator":
292                    self.parseSeparator()
293                elif child.tagName == "Filename":
294                    try:
295                        self.parseFilename(child.childNodes[0].nodeValue)
296                    except IndexError:
297                        raise ValidationError('Filename cannot be empty', "")
298                elif child.tagName == "Merge":
299                    self.parseMerge(child.getAttribute("type") or "all")
300
301    def parseMenuname(self, value, empty="false", inline="false", inline_limit=4, inline_header="true", inline_alias="false"):
302        self.order.append(["Menuname", value, empty, inline, inline_limit, inline_header, inline_alias])
303        self.order[-1][4] = int(self.order[-1][4])
304
305    def parseSeparator(self):
306        self.order.append(["Separator"])
307
308    def parseFilename(self, value):
309        self.order.append(["Filename", value])
310
311    def parseMerge(self, type="all"):
312        self.order.append(["Merge", type])
313
314
315class Rule:
316    "Inlcude / Exclude Rules Class"
317    def __init__(self, type, node=None):
318        # Type is Include or Exclude
319        self.Type = type
320        # Rule is a python expression
321        self.Rule = ""
322
323        # Private attributes, only needed for parsing
324        self.Depth = 0
325        self.Expr = [ "or" ]
326        self.New = True
327
328        # Begin parsing
329        if node:
330            self.parseNode(node)
331
332    def __str__(self):
333        return self.Rule
334
335    def do(self, menuentries, type, run):
336        for menuentry in menuentries:
337            if run == 2 and ( menuentry.MatchedInclude == True \
338            or menuentry.Allocated == True ):
339                continue
340            elif eval(self.Rule):
341                if type == "Include":
342                    menuentry.Add = True
343                    menuentry.MatchedInclude = True
344                else:
345                    menuentry.Add = False
346        return menuentries
347
348    def parseNode(self, node):
349        for child in node.childNodes:
350            if child.nodeType == ELEMENT_NODE:
351                if child.tagName == 'Filename':
352                    try:
353                        self.parseFilename(child.childNodes[0].nodeValue)
354                    except IndexError:
355                        raise ValidationError('Filename cannot be empty', "???")
356                elif child.tagName == 'Category':
357                    try:
358                        self.parseCategory(child.childNodes[0].nodeValue)
359                    except IndexError:
360                        raise ValidationError('Category cannot be empty', "???")
361                elif child.tagName == 'All':
362                    self.parseAll()
363                elif child.tagName == 'And':
364                    self.parseAnd(child)
365                elif child.tagName == 'Or':
366                    self.parseOr(child)
367                elif child.tagName == 'Not':
368                    self.parseNot(child)
369
370    def parseNew(self, set=True):
371        if not self.New:
372            self.Rule += " " + self.Expr[self.Depth] + " "
373        if not set:
374            self.New = True
375        elif set:
376            self.New = False
377
378    def parseFilename(self, value):
379        self.parseNew()
380        self.Rule += "menuentry.DesktopFileID == '%s'" % value.strip().replace("\\", r"\\").replace("'", r"\'")
381
382    def parseCategory(self, value):
383        self.parseNew()
384        self.Rule += "'%s' in menuentry.Categories" % value.strip()
385
386    def parseAll(self):
387        self.parseNew()
388        self.Rule += "True"
389
390    def parseAnd(self, node):
391        self.parseNew(False)
392        self.Rule += "("
393        self.Depth += 1
394        self.Expr.append("and")
395        self.parseNode(node)
396        self.Depth -= 1
397        self.Expr.pop()
398        self.Rule += ")"
399
400    def parseOr(self, node):
401        self.parseNew(False)
402        self.Rule += "("
403        self.Depth += 1
404        self.Expr.append("or")
405        self.parseNode(node)
406        self.Depth -= 1
407        self.Expr.pop()
408        self.Rule += ")"
409
410    def parseNot(self, node):
411        self.parseNew(False)
412        self.Rule += "not ("
413        self.Depth += 1
414        self.Expr.append("or")
415        self.parseNode(node)
416        self.Depth -= 1
417        self.Expr.pop()
418        self.Rule += ")"
419
420
421class MenuEntry:
422    "Wrapper for 'Menu Style' Desktop Entries"
423    def __init__(self, filename, dir="", prefix=""):
424        # Create entry
425        self.DesktopEntry = DesktopEntry(os.path.join(dir,filename))
426        self.setAttributes(filename, dir, prefix)
427
428        # Can be one of Deleted/Hidden/Empty/NotShowIn/NoExec or True
429        self.Show = True
430
431        # Semi-Private
432        self.Original = None
433        self.Parents = []
434
435        # Private Stuff
436        self.Allocated = False
437        self.Add = False
438        self.MatchedInclude = False
439
440        # Caching
441        self.Categories = self.DesktopEntry.getCategories()
442
443    def save(self):
444        """Save any changes to the desktop entry."""
445        if self.DesktopEntry.tainted == True:
446            self.DesktopEntry.write()
447
448    def getDir(self):
449        """Return the directory containing the desktop entry file."""
450        return self.DesktopEntry.filename.replace(self.Filename, '')
451
452    def getType(self):
453        """Return the type of MenuEntry, System/User/Both"""
454        if xdg.Config.root_mode == False:
455            if self.Original:
456                return "Both"
457            elif xdg_data_dirs[0] in self.DesktopEntry.filename:
458                return "User"
459            else:
460                return "System"
461        else:
462            return "User"
463
464    def setAttributes(self, filename, dir="", prefix=""):
465        self.Filename = filename
466        self.Prefix = prefix
467        self.DesktopFileID = os.path.join(prefix,filename).replace("/", "-")
468
469        if not os.path.isabs(self.DesktopEntry.filename):
470            self.__setFilename()
471
472    def updateAttributes(self):
473        if self.getType() == "System":
474            self.Original = MenuEntry(self.Filename, self.getDir(), self.Prefix)
475            self.__setFilename()
476
477    def __setFilename(self):
478        if xdg.Config.root_mode == False:
479            path = xdg_data_dirs[0]
480        else:
481            path= xdg_data_dirs[1]
482
483        if self.DesktopEntry.getType() == "Application":
484            dir = os.path.join(path, "applications")
485        else:
486            dir = os.path.join(path, "desktop-directories")
487
488        self.DesktopEntry.filename = os.path.join(dir, self.Filename)
489
490    def __cmp__(self, other):
491        return locale.strcoll(self.DesktopEntry.getName(), other.DesktopEntry.getName())
492
493    def _key(self):
494        """Key function for locale-aware sorting."""
495        return _strxfrm(self.DesktopEntry.getName())
496
497    def __lt__(self, other):
498        try:
499            other = other._key()
500        except AttributeError:
501            pass
502        return self._key() < other
503
504
505    def __eq__(self, other):
506        if self.DesktopFileID == str(other):
507            return True
508        else:
509            return False
510
511    def __repr__(self):
512        return self.DesktopFileID
513
514
515class Separator:
516    "Just a dummy class for Separators"
517    def __init__(self, parent):
518        self.Parent = parent
519        self.Show = True
520
521
522class Header:
523    "Class for Inline Headers"
524    def __init__(self, name, generic_name, comment):
525        self.Name = name
526        self.GenericName = generic_name
527        self.Comment = comment
528
529    def __str__(self):
530        return self.Name
531
532
533tmp = {}
534
535def __getFileName(filename):
536    dirs = xdg_config_dirs[:]
537    if xdg.Config.root_mode == True:
538        dirs.pop(0)
539
540    for dir in dirs:
541        menuname = os.path.join (dir, "menus" , filename)
542        if os.path.isdir(dir) and os.path.isfile(menuname):
543            return menuname
544
545def parse(filename=None):
546    """Load an applications.menu file.
547
548    filename : str, optional
549      The default is ``$XDG_CONFIG_DIRS/menus/${XDG_MENU_PREFIX}applications.menu``.
550    """
551    # convert to absolute path
552    if filename and not os.path.isabs(filename):
553        filename = __getFileName(filename)
554
555    # use default if no filename given
556    if not filename:
557        candidate = os.environ.get('XDG_MENU_PREFIX', '') + "applications.menu"
558        filename = __getFileName(candidate)
559
560    if not filename:
561        raise ParsingError('File not found', "/etc/xdg/menus/%s" % candidate)
562
563    # check if it is a .menu file
564    if not os.path.splitext(filename)[1] == ".menu":
565        raise ParsingError('Not a .menu file', filename)
566
567    # create xml parser
568    try:
569        doc = xml.dom.minidom.parse(filename)
570    except xml.parsers.expat.ExpatError:
571        raise ParsingError('Not a valid .menu file', filename)
572
573    # parse menufile
574    tmp["Root"] = ""
575    tmp["mergeFiles"] = []
576    tmp["DirectoryDirs"] = []
577    tmp["cache"] = MenuEntryCache()
578
579    __parse(doc, filename, tmp["Root"])
580    __parsemove(tmp["Root"])
581    __postparse(tmp["Root"])
582
583    tmp["Root"].Doc = doc
584    tmp["Root"].Filename = filename
585
586    # generate the menu
587    __genmenuNotOnlyAllocated(tmp["Root"])
588    __genmenuOnlyAllocated(tmp["Root"])
589
590    # and finally sort
591    sort(tmp["Root"])
592
593    return tmp["Root"]
594
595
596def __parse(node, filename, parent=None):
597    for child in node.childNodes:
598        if child.nodeType == ELEMENT_NODE:
599            if child.tagName == 'Menu':
600                __parseMenu(child, filename, parent)
601            elif child.tagName == 'AppDir':
602                try:
603                    __parseAppDir(child.childNodes[0].nodeValue, filename, parent)
604                except IndexError:
605                    raise ValidationError('AppDir cannot be empty', filename)
606            elif child.tagName == 'DefaultAppDirs':
607                __parseDefaultAppDir(filename, parent)
608            elif child.tagName == 'DirectoryDir':
609                try:
610                    __parseDirectoryDir(child.childNodes[0].nodeValue, filename, parent)
611                except IndexError:
612                    raise ValidationError('DirectoryDir cannot be empty', filename)
613            elif child.tagName == 'DefaultDirectoryDirs':
614                __parseDefaultDirectoryDir(filename, parent)
615            elif child.tagName == 'Name' :
616                try:
617                    parent.Name = child.childNodes[0].nodeValue
618                except IndexError:
619                    raise ValidationError('Name cannot be empty', filename)
620            elif child.tagName == 'Directory' :
621                try:
622                    parent.Directories.append(child.childNodes[0].nodeValue)
623                except IndexError:
624                    raise ValidationError('Directory cannot be empty', filename)
625            elif child.tagName == 'OnlyUnallocated':
626                parent.OnlyUnallocated = True
627            elif child.tagName == 'NotOnlyUnallocated':
628                parent.OnlyUnallocated = False
629            elif child.tagName == 'Deleted':
630                parent.Deleted = True
631            elif child.tagName == 'NotDeleted':
632                parent.Deleted = False
633            elif child.tagName == 'Include' or child.tagName == 'Exclude':
634                parent.Rules.append(Rule(child.tagName, child))
635            elif child.tagName == 'MergeFile':
636                try:
637                    if child.getAttribute("type") == "parent":
638                        __parseMergeFile("applications.menu", child, filename, parent)
639                    else:
640                        __parseMergeFile(child.childNodes[0].nodeValue, child, filename, parent)
641                except IndexError:
642                    raise ValidationError('MergeFile cannot be empty', filename)
643            elif child.tagName == 'MergeDir':
644                try:
645                    __parseMergeDir(child.childNodes[0].nodeValue, child, filename, parent)
646                except IndexError:
647                    raise ValidationError('MergeDir cannot be empty', filename)
648            elif child.tagName == 'DefaultMergeDirs':
649                __parseDefaultMergeDirs(child, filename, parent)
650            elif child.tagName == 'Move':
651                parent.Moves.append(Move(child))
652            elif child.tagName == 'Layout':
653                if len(child.childNodes) > 1:
654                    parent.Layout = Layout(child)
655            elif child.tagName == 'DefaultLayout':
656                if len(child.childNodes) > 1:
657                    parent.DefaultLayout = Layout(child)
658            elif child.tagName == 'LegacyDir':
659                try:
660                    __parseLegacyDir(child.childNodes[0].nodeValue, child.getAttribute("prefix"), filename, parent)
661                except IndexError:
662                    raise ValidationError('LegacyDir cannot be empty', filename)
663            elif child.tagName == 'KDELegacyDirs':
664                __parseKDELegacyDirs(filename, parent)
665
666def __parsemove(menu):
667    for submenu in menu.Submenus:
668        __parsemove(submenu)
669
670    # parse move operations
671    for move in menu.Moves:
672        move_from_menu = menu.getMenu(move.Old)
673        if move_from_menu:
674            move_to_menu = menu.getMenu(move.New)
675
676            menus = move.New.split("/")
677            oldparent = None
678            while len(menus) > 0:
679                if not oldparent:
680                    oldparent = menu
681                newmenu = oldparent.getMenu(menus[0])
682                if not newmenu:
683                    newmenu = Menu()
684                    newmenu.Name = menus[0]
685                    if len(menus) > 1:
686                        newmenu.NotInXml = True
687                    oldparent.addSubmenu(newmenu)
688                oldparent = newmenu
689                menus.pop(0)
690
691            newmenu += move_from_menu
692            move_from_menu.Parent.Submenus.remove(move_from_menu)
693
694def __postparse(menu):
695    # unallocated / deleted
696    if menu.Deleted == "notset":
697        menu.Deleted = False
698    if menu.OnlyUnallocated == "notset":
699        menu.OnlyUnallocated = False
700
701    # Layout Tags
702    if not menu.Layout or not menu.DefaultLayout:
703        if menu.DefaultLayout:
704            menu.Layout = menu.DefaultLayout
705        elif menu.Layout:
706            if menu.Depth > 0:
707                menu.DefaultLayout = menu.Parent.DefaultLayout
708            else:
709                menu.DefaultLayout = Layout()
710        else:
711            if menu.Depth > 0:
712                menu.Layout = menu.Parent.DefaultLayout
713                menu.DefaultLayout = menu.Parent.DefaultLayout
714            else:
715                menu.Layout = Layout()
716                menu.DefaultLayout = Layout()
717
718    # add parent's app/directory dirs
719    if menu.Depth > 0:
720        menu.AppDirs = menu.Parent.AppDirs + menu.AppDirs
721        menu.DirectoryDirs = menu.Parent.DirectoryDirs + menu.DirectoryDirs
722
723    # remove duplicates
724    menu.Directories = __removeDuplicates(menu.Directories)
725    menu.DirectoryDirs = __removeDuplicates(menu.DirectoryDirs)
726    menu.AppDirs = __removeDuplicates(menu.AppDirs)
727
728    # go recursive through all menus
729    for submenu in menu.Submenus:
730        __postparse(submenu)
731
732    # reverse so handling is easier
733    menu.Directories.reverse()
734    menu.DirectoryDirs.reverse()
735    menu.AppDirs.reverse()
736
737    # get the valid .directory file out of the list
738    for directory in menu.Directories:
739        for dir in menu.DirectoryDirs:
740            if os.path.isfile(os.path.join(dir, directory)):
741                menuentry = MenuEntry(directory, dir)
742                if not menu.Directory:
743                    menu.Directory = menuentry
744                elif menuentry.getType() == "System":
745                    if menu.Directory.getType() == "User":
746                        menu.Directory.Original = menuentry
747        if menu.Directory:
748            break
749
750
751# Menu parsing stuff
752def __parseMenu(child, filename, parent):
753    m = Menu()
754    __parse(child, filename, m)
755    if parent:
756        parent.addSubmenu(m)
757    else:
758        tmp["Root"] = m
759
760# helper function
761def __check(value, filename, type):
762    path = os.path.dirname(filename)
763
764    if not os.path.isabs(value):
765        value = os.path.join(path, value)
766
767    value = os.path.abspath(value)
768
769    if type == "dir" and os.path.exists(value) and os.path.isdir(value):
770        return value
771    elif type == "file" and os.path.exists(value) and os.path.isfile(value):
772        return value
773    else:
774        return False
775
776# App/Directory Dir Stuff
777def __parseAppDir(value, filename, parent):
778    value = __check(value, filename, "dir")
779    if value:
780        parent.AppDirs.append(value)
781
782def __parseDefaultAppDir(filename, parent):
783    for dir in reversed(xdg_data_dirs):
784        __parseAppDir(os.path.join(dir, "applications"), filename, parent)
785
786def __parseDirectoryDir(value, filename, parent):
787    value = __check(value, filename, "dir")
788    if value:
789        parent.DirectoryDirs.append(value)
790
791def __parseDefaultDirectoryDir(filename, parent):
792    for dir in reversed(xdg_data_dirs):
793        __parseDirectoryDir(os.path.join(dir, "desktop-directories"), filename, parent)
794
795# Merge Stuff
796def __parseMergeFile(value, child, filename, parent):
797    if child.getAttribute("type") == "parent":
798        for dir in xdg_config_dirs:
799            rel_file = filename.replace(dir, "").strip("/")
800            if rel_file != filename:
801                for p in xdg_config_dirs:
802                    if dir == p:
803                        continue
804                    if os.path.isfile(os.path.join(p,rel_file)):
805                        __mergeFile(os.path.join(p,rel_file),child,parent)
806                        break
807    else:
808        value = __check(value, filename, "file")
809        if value:
810            __mergeFile(value, child, parent)
811
812def __parseMergeDir(value, child, filename, parent):
813    value = __check(value, filename, "dir")
814    if value:
815        for item in os.listdir(value):
816            try:
817                if os.path.splitext(item)[1] == ".menu":
818                    __mergeFile(os.path.join(value, item), child, parent)
819            except UnicodeDecodeError:
820                continue
821
822def __parseDefaultMergeDirs(child, filename, parent):
823    basename = os.path.splitext(os.path.basename(filename))[0]
824    for dir in reversed(xdg_config_dirs):
825        __parseMergeDir(os.path.join(dir, "menus", basename + "-merged"), child, filename, parent)
826
827def __mergeFile(filename, child, parent):
828    # check for infinite loops
829    if filename in tmp["mergeFiles"]:
830        if debug:
831            raise ParsingError('Infinite MergeFile loop detected', filename)
832        else:
833            return
834
835    tmp["mergeFiles"].append(filename)
836
837    # load file
838    try:
839        doc = xml.dom.minidom.parse(filename)
840    except IOError:
841        if debug:
842            raise ParsingError('File not found', filename)
843        else:
844            return
845    except xml.parsers.expat.ExpatError:
846        if debug:
847            raise ParsingError('Not a valid .menu file', filename)
848        else:
849            return
850
851    # append file
852    for child in doc.childNodes:
853        if child.nodeType == ELEMENT_NODE:
854            __parse(child,filename,parent)
855            break
856
857# Legacy Dir Stuff
858def __parseLegacyDir(dir, prefix, filename, parent):
859    m = __mergeLegacyDir(dir,prefix,filename,parent)
860    if m:
861        parent += m
862
863def __mergeLegacyDir(dir, prefix, filename, parent):
864    dir = __check(dir,filename,"dir")
865    if dir and dir not in tmp["DirectoryDirs"]:
866        tmp["DirectoryDirs"].append(dir)
867
868        m = Menu()
869        m.AppDirs.append(dir)
870        m.DirectoryDirs.append(dir)
871        m.Name = os.path.basename(dir)
872        m.NotInXml = True
873
874        for item in os.listdir(dir):
875            try:
876                if item == ".directory":
877                    m.Directories.append(item)
878                elif os.path.isdir(os.path.join(dir,item)):
879                    m.addSubmenu(__mergeLegacyDir(os.path.join(dir,item), prefix, filename, parent))
880            except UnicodeDecodeError:
881                continue
882
883        tmp["cache"].addMenuEntries([dir],prefix, True)
884        menuentries = tmp["cache"].getMenuEntries([dir], False)
885
886        for menuentry in menuentries:
887            categories = menuentry.Categories
888            if len(categories) == 0:
889                r = Rule("Include")
890                r.parseFilename(menuentry.DesktopFileID)
891                m.Rules.append(r)
892            if not dir in parent.AppDirs:
893                categories.append("Legacy")
894                menuentry.Categories = categories
895
896        return m
897
898def __parseKDELegacyDirs(filename, parent):
899    try:
900        proc = subprocess.Popen(['kde-config', '--path', 'apps'],
901                                stdout=subprocess.PIPE, universal_newlines=True)
902        output = proc.communicate()[0].splitlines()
903    except OSError:
904        # If kde-config doesn't exist, ignore this.
905        return
906
907    try:
908        for dir in output[0].split(":"):
909            __parseLegacyDir(dir,"kde", filename, parent)
910    except IndexError:
911        pass
912
913# remove duplicate entries from a list
914def __removeDuplicates(list):
915    set = {}
916    list.reverse()
917    list = [set.setdefault(e,e) for e in list if e not in set]
918    list.reverse()
919    return list
920
921# Finally generate the menu
922def __genmenuNotOnlyAllocated(menu):
923    for submenu in menu.Submenus:
924        __genmenuNotOnlyAllocated(submenu)
925
926    if menu.OnlyUnallocated == False:
927        tmp["cache"].addMenuEntries(menu.AppDirs)
928        menuentries = []
929        for rule in menu.Rules:
930            menuentries = rule.do(tmp["cache"].getMenuEntries(menu.AppDirs), rule.Type, 1)
931        for menuentry in menuentries:
932            if menuentry.Add == True:
933                menuentry.Parents.append(menu)
934                menuentry.Add = False
935                menuentry.Allocated = True
936                menu.MenuEntries.append(menuentry)
937
938def __genmenuOnlyAllocated(menu):
939    for submenu in menu.Submenus:
940        __genmenuOnlyAllocated(submenu)
941
942    if menu.OnlyUnallocated == True:
943        tmp["cache"].addMenuEntries(menu.AppDirs)
944        menuentries = []
945        for rule in menu.Rules:
946            menuentries = rule.do(tmp["cache"].getMenuEntries(menu.AppDirs), rule.Type, 2)
947        for menuentry in menuentries:
948            if menuentry.Add == True:
949                menuentry.Parents.append(menu)
950            #   menuentry.Add = False
951            #   menuentry.Allocated = True
952                menu.MenuEntries.append(menuentry)
953
954# And sorting ...
955def sort(menu):
956    menu.Entries = []
957    menu.Visible = 0
958
959    for submenu in menu.Submenus:
960        sort(submenu)
961
962    tmp_s = []
963    tmp_e = []
964
965    for order in menu.Layout.order:
966        if order[0] == "Filename":
967            tmp_e.append(order[1])
968        elif order[0] == "Menuname":
969            tmp_s.append(order[1])
970
971    for order in menu.Layout.order:
972        if order[0] == "Separator":
973            separator = Separator(menu)
974            if len(menu.Entries) > 0 and isinstance(menu.Entries[-1], Separator):
975                separator.Show = False
976            menu.Entries.append(separator)
977        elif order[0] == "Filename":
978            menuentry = menu.getMenuEntry(order[1])
979            if menuentry:
980                menu.Entries.append(menuentry)
981        elif order[0] == "Menuname":
982            submenu = menu.getMenu(order[1])
983            if submenu:
984                __parse_inline(submenu, menu)
985        elif order[0] == "Merge":
986            if order[1] == "files" or order[1] == "all":
987                menu.MenuEntries.sort()
988                for menuentry in menu.MenuEntries:
989                    if menuentry not in tmp_e:
990                        menu.Entries.append(menuentry)
991            elif order[1] == "menus" or order[1] == "all":
992                menu.Submenus.sort()
993                for submenu in menu.Submenus:
994                    if submenu.Name not in tmp_s:
995                        __parse_inline(submenu, menu)
996
997    # getHidden / NoDisplay / OnlyShowIn / NotOnlyShowIn / Deleted / NoExec
998    for entry in menu.Entries:
999        entry.Show = True
1000        menu.Visible += 1
1001        if isinstance(entry, Menu):
1002            if entry.Deleted == True:
1003                entry.Show = "Deleted"
1004                menu.Visible -= 1
1005            elif isinstance(entry.Directory, MenuEntry):
1006                if entry.Directory.DesktopEntry.getNoDisplay() == True:
1007                    entry.Show = "NoDisplay"
1008                    menu.Visible -= 1
1009                elif entry.Directory.DesktopEntry.getHidden() == True:
1010                    entry.Show = "Hidden"
1011                    menu.Visible -= 1
1012        elif isinstance(entry, MenuEntry):
1013            if entry.DesktopEntry.getNoDisplay() == True:
1014                entry.Show = "NoDisplay"
1015                menu.Visible -= 1
1016            elif entry.DesktopEntry.getHidden() == True:
1017                entry.Show = "Hidden"
1018                menu.Visible -= 1
1019            elif entry.DesktopEntry.getTryExec() and not __try_exec(entry.DesktopEntry.getTryExec()):
1020                entry.Show = "NoExec"
1021                menu.Visible -= 1
1022            elif xdg.Config.windowmanager:
1023                if ( entry.DesktopEntry.getOnlyShowIn() != [] and xdg.Config.windowmanager not in entry.DesktopEntry.getOnlyShowIn() ) \
1024                or xdg.Config.windowmanager in entry.DesktopEntry.getNotShowIn():
1025                    entry.Show = "NotShowIn"
1026                    menu.Visible -= 1
1027        elif isinstance(entry,Separator):
1028            menu.Visible -= 1
1029
1030    # remove separators at the beginning and at the end
1031    if len(menu.Entries) > 0:
1032        if isinstance(menu.Entries[0], Separator):
1033            menu.Entries[0].Show = False
1034    if len(menu.Entries) > 1:
1035        if isinstance(menu.Entries[-1], Separator):
1036            menu.Entries[-1].Show = False
1037
1038    # show_empty tag
1039    for entry in menu.Entries[:]:
1040        if isinstance(entry, Menu) and entry.Layout.show_empty == "false" and entry.Visible == 0:
1041            entry.Show = "Empty"
1042            menu.Visible -= 1
1043            if entry.NotInXml == True:
1044                menu.Entries.remove(entry)
1045
1046def __try_exec(executable):
1047    paths = os.environ['PATH'].split(os.pathsep)
1048    if not os.path.isfile(executable):
1049        for p in paths:
1050            f = os.path.join(p, executable)
1051            if os.path.isfile(f):
1052                if os.access(f, os.X_OK):
1053                    return True
1054    else:
1055        if os.access(executable, os.X_OK):
1056            return True
1057    return False
1058
1059# inline tags
1060def __parse_inline(submenu, menu):
1061    if submenu.Layout.inline == "true":
1062        if len(submenu.Entries) == 1 and submenu.Layout.inline_alias == "true":
1063            menuentry = submenu.Entries[0]
1064            menuentry.DesktopEntry.set("Name", submenu.getName(), locale = True)
1065            menuentry.DesktopEntry.set("GenericName", submenu.getGenericName(), locale = True)
1066            menuentry.DesktopEntry.set("Comment", submenu.getComment(), locale = True)
1067            menu.Entries.append(menuentry)
1068        elif len(submenu.Entries) <= submenu.Layout.inline_limit or submenu.Layout.inline_limit == 0:
1069            if submenu.Layout.inline_header == "true":
1070                header = Header(submenu.getName(), submenu.getGenericName(), submenu.getComment())
1071                menu.Entries.append(header)
1072            for entry in submenu.Entries:
1073                menu.Entries.append(entry)
1074        else:
1075            menu.Entries.append(submenu)
1076    else:
1077        menu.Entries.append(submenu)
1078
1079class MenuEntryCache:
1080    "Class to cache Desktop Entries"
1081    def __init__(self):
1082        self.cacheEntries = {}
1083        self.cacheEntries['legacy'] = []
1084        self.cache = {}
1085
1086    def addMenuEntries(self, dirs, prefix="", legacy=False):
1087        for dir in dirs:
1088            if not dir in self.cacheEntries:
1089                self.cacheEntries[dir] = []
1090                self.__addFiles(dir, "", prefix, legacy)
1091
1092    def __addFiles(self, dir, subdir, prefix, legacy):
1093        for item in os.listdir(os.path.join(dir,subdir)):
1094            if os.path.splitext(item)[1] == ".desktop":
1095                try:
1096                    menuentry = MenuEntry(os.path.join(subdir,item), dir, prefix)
1097                except ParsingError:
1098                    continue
1099
1100                self.cacheEntries[dir].append(menuentry)
1101                if legacy == True:
1102                    self.cacheEntries['legacy'].append(menuentry)
1103            elif os.path.isdir(os.path.join(dir,subdir,item)) and legacy == False:
1104                self.__addFiles(dir, os.path.join(subdir,item), prefix, legacy)
1105
1106    def getMenuEntries(self, dirs, legacy=True):
1107        list = []
1108        ids = []
1109        # handle legacy items
1110        appdirs = dirs[:]
1111        if legacy == True:
1112            appdirs.append("legacy")
1113        # cache the results again
1114        key = "".join(appdirs)
1115        try:
1116            return self.cache[key]
1117        except KeyError:
1118            pass
1119        for dir in appdirs:
1120            for menuentry in self.cacheEntries[dir]:
1121                try:
1122                    if menuentry.DesktopFileID not in ids:
1123                        ids.append(menuentry.DesktopFileID)
1124                        list.append(menuentry)
1125                    elif menuentry.getType() == "System":
1126                    # FIXME: This is only 99% correct, but still...
1127                        i = list.index(menuentry)
1128                        e = list[i]
1129                        if e.getType() == "User":
1130                            e.Original = menuentry
1131                except UnicodeDecodeError:
1132                    continue
1133        self.cache[key] = list
1134        return list
1135