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