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