Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/xdg/Menu.py: 66%

791 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-07 06:37 +0000

1""" 

2Implementation of the XDG Menu Specification 

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 os 

21import locale 

22import subprocess 

23import ast 

24import sys 

25try: 

26 import xml.etree.cElementTree as etree 

27except ImportError: 

28 import xml.etree.ElementTree as etree 

29 

30from xdg.BaseDirectory import xdg_data_dirs, xdg_config_dirs 

31from xdg.DesktopEntry import DesktopEntry 

32from xdg.Exceptions import ParsingError 

33from xdg.util import PY3 

34 

35import xdg.Locale 

36import xdg.Config 

37 

38 

39def _ast_const(name): 

40 if sys.version_info >= (3, 4): 

41 name = ast.literal_eval(name) 

42 if sys.version_info >= (3, 8): 

43 return ast.Constant(name) 

44 else: 

45 return ast.NameConstant(name) 

46 else: 

47 return ast.Name(id=name, ctx=ast.Load()) 

48 

49 

50def _strxfrm(s): 

51 """Wrapper around locale.strxfrm that accepts unicode strings on Python 2. 

52 

53 See Python bug #2481. 

54 """ 

55 if (not PY3) and isinstance(s, unicode): 

56 s = s.encode('utf-8') 

57 return locale.strxfrm(s) 

58 

59 

60DELETED = "Deleted" 

61NO_DISPLAY = "NoDisplay" 

62HIDDEN = "Hidden" 

63EMPTY = "Empty" 

64NOT_SHOW_IN = "NotShowIn" 

65NO_EXEC = "NoExec" 

66 

67 

68class Menu: 

69 """Menu containing sub menus under menu.Entries 

70 

71 Contains both Menu and MenuEntry items. 

72 """ 

73 def __init__(self): 

74 # Public stuff 

75 self.Name = "" 

76 self.Directory = None 

77 self.Entries = [] 

78 self.Doc = "" 

79 self.Filename = "" 

80 self.Depth = 0 

81 self.Parent = None 

82 self.NotInXml = False 

83 

84 # Can be True, False, DELETED, NO_DISPLAY, HIDDEN, EMPTY or NOT_SHOW_IN 

85 self.Show = True 

86 self.Visible = 0 

87 

88 # Private stuff, only needed for parsing 

89 self.AppDirs = [] 

90 self.DefaultLayout = None 

91 self.Deleted = None 

92 self.Directories = [] 

93 self.DirectoryDirs = [] 

94 self.Layout = None 

95 self.MenuEntries = [] 

96 self.Moves = [] 

97 self.OnlyUnallocated = None 

98 self.Rules = [] 

99 self.Submenus = [] 

100 

101 def __str__(self): 

102 return self.Name 

103 

104 def __add__(self, other): 

105 for dir in other.AppDirs: 

106 self.AppDirs.append(dir) 

107 

108 for dir in other.DirectoryDirs: 

109 self.DirectoryDirs.append(dir) 

110 

111 for directory in other.Directories: 

112 self.Directories.append(directory) 

113 

114 if other.Deleted is not None: 

115 self.Deleted = other.Deleted 

116 

117 if other.OnlyUnallocated is not None: 

118 self.OnlyUnallocated = other.OnlyUnallocated 

119 

120 if other.Layout: 

121 self.Layout = other.Layout 

122 

123 if other.DefaultLayout: 

124 self.DefaultLayout = other.DefaultLayout 

125 

126 for rule in other.Rules: 

127 self.Rules.append(rule) 

128 

129 for move in other.Moves: 

130 self.Moves.append(move) 

131 

132 for submenu in other.Submenus: 

133 self.addSubmenu(submenu) 

134 

135 return self 

136 

137 # FIXME: Performance: cache getName() 

138 def __cmp__(self, other): 

139 return locale.strcoll(self.getName(), other.getName()) 

140 

141 def _key(self): 

142 """Key function for locale-aware sorting.""" 

143 return _strxfrm(self.getName()) 

144 

145 def __lt__(self, other): 

146 try: 

147 other = other._key() 

148 except AttributeError: 

149 pass 

150 return self._key() < other 

151 

152 def __eq__(self, other): 

153 try: 

154 return self.Name == unicode(other) 

155 except NameError: # unicode() becomes str() in Python 3 

156 return self.Name == str(other) 

157 

158 """ PUBLIC STUFF """ 

159 def getEntries(self, show_hidden=False): 

160 """Interator for a list of Entries visible to the user.""" 

161 for entry in self.Entries: 

162 if show_hidden: 

163 yield entry 

164 elif entry.Show is True: 

165 yield entry 

166 

167 # FIXME: Add searchEntry/seaqrchMenu function 

168 # search for name/comment/genericname/desktopfileid 

169 # return multiple items 

170 

171 def getMenuEntry(self, desktopfileid, deep=False): 

172 """Searches for a MenuEntry with a given DesktopFileID.""" 

173 for menuentry in self.MenuEntries: 

174 if menuentry.DesktopFileID == desktopfileid: 

175 return menuentry 

176 if deep: 

177 for submenu in self.Submenus: 

178 submenu.getMenuEntry(desktopfileid, deep) 

179 

180 def getMenu(self, path): 

181 """Searches for a Menu with a given path.""" 

182 array = path.split("/", 1) 

183 for submenu in self.Submenus: 

184 if submenu.Name == array[0]: 

185 if len(array) > 1: 

186 return submenu.getMenu(array[1]) 

187 else: 

188 return submenu 

189 

190 def getPath(self, org=False, toplevel=False): 

191 """Returns this menu's path in the menu structure.""" 

192 parent = self 

193 names = [] 

194 while 1: 

195 if org: 

196 names.append(parent.Name) 

197 else: 

198 names.append(parent.getName()) 

199 if parent.Depth > 0: 

200 parent = parent.Parent 

201 else: 

202 break 

203 names.reverse() 

204 path = "" 

205 if not toplevel: 

206 names.pop(0) 

207 for name in names: 

208 path = os.path.join(path, name) 

209 return path 

210 

211 def getName(self): 

212 """Returns the menu's localised name.""" 

213 try: 

214 return self.Directory.DesktopEntry.getName() 

215 except AttributeError: 

216 return self.Name 

217 

218 def getGenericName(self): 

219 """Returns the menu's generic name.""" 

220 try: 

221 return self.Directory.DesktopEntry.getGenericName() 

222 except AttributeError: 

223 return "" 

224 

225 def getComment(self): 

226 """Returns the menu's comment text.""" 

227 try: 

228 return self.Directory.DesktopEntry.getComment() 

229 except AttributeError: 

230 return "" 

231 

232 def getIcon(self): 

233 """Returns the menu's icon, filename or simple name""" 

234 try: 

235 return self.Directory.DesktopEntry.getIcon() 

236 except AttributeError: 

237 return "" 

238 

239 def sort(self): 

240 self.Entries = [] 

241 self.Visible = 0 

242 

243 for submenu in self.Submenus: 

244 submenu.sort() 

245 

246 _submenus = set() 

247 _entries = set() 

248 

249 for order in self.Layout.order: 

250 if order[0] == "Filename": 

251 _entries.add(order[1]) 

252 elif order[0] == "Menuname": 

253 _submenus.add(order[1]) 

254 

255 for order in self.Layout.order: 

256 if order[0] == "Separator": 

257 separator = Separator(self) 

258 if len(self.Entries) > 0 and isinstance(self.Entries[-1], Separator): 

259 separator.Show = False 

260 self.Entries.append(separator) 

261 elif order[0] == "Filename": 

262 menuentry = self.getMenuEntry(order[1]) 

263 if menuentry: 

264 self.Entries.append(menuentry) 

265 elif order[0] == "Menuname": 

266 submenu = self.getMenu(order[1]) 

267 if submenu: 

268 if submenu.Layout.inline: 

269 self.merge_inline(submenu) 

270 else: 

271 self.Entries.append(submenu) 

272 elif order[0] == "Merge": 

273 if order[1] == "files" or order[1] == "all": 

274 self.MenuEntries.sort() 

275 for menuentry in self.MenuEntries: 

276 if menuentry.DesktopFileID not in _entries: 

277 self.Entries.append(menuentry) 

278 elif order[1] == "menus" or order[1] == "all": 

279 self.Submenus.sort() 

280 for submenu in self.Submenus: 

281 if submenu.Name not in _submenus: 

282 if submenu.Layout.inline: 

283 self.merge_inline(submenu) 

284 else: 

285 self.Entries.append(submenu) 

286 

287 # getHidden / NoDisplay / OnlyShowIn / NotOnlyShowIn / Deleted / NoExec 

288 for entry in self.Entries: 

289 entry.Show = True 

290 self.Visible += 1 

291 if isinstance(entry, Menu): 

292 if entry.Deleted is True: 

293 entry.Show = DELETED 

294 self.Visible -= 1 

295 elif isinstance(entry.Directory, MenuEntry): 

296 if entry.Directory.DesktopEntry.getNoDisplay(): 

297 entry.Show = NO_DISPLAY 

298 self.Visible -= 1 

299 elif entry.Directory.DesktopEntry.getHidden(): 

300 entry.Show = HIDDEN 

301 self.Visible -= 1 

302 elif isinstance(entry, MenuEntry): 

303 if entry.DesktopEntry.getNoDisplay(): 

304 entry.Show = NO_DISPLAY 

305 self.Visible -= 1 

306 elif entry.DesktopEntry.getHidden(): 

307 entry.Show = HIDDEN 

308 self.Visible -= 1 

309 elif entry.DesktopEntry.getTryExec() and not entry.DesktopEntry.findTryExec(): 

310 entry.Show = NO_EXEC 

311 self.Visible -= 1 

312 elif xdg.Config.windowmanager: 

313 if (entry.DesktopEntry.getOnlyShowIn() != [] and ( 

314 xdg.Config.windowmanager not in entry.DesktopEntry.getOnlyShowIn() 

315 ) 

316 ) or ( 

317 xdg.Config.windowmanager in entry.DesktopEntry.getNotShowIn() 

318 ): 

319 entry.Show = NOT_SHOW_IN 

320 self.Visible -= 1 

321 elif isinstance(entry, Separator): 

322 self.Visible -= 1 

323 # remove separators at the beginning and at the end 

324 if len(self.Entries) > 0: 

325 if isinstance(self.Entries[0], Separator): 

326 self.Entries[0].Show = False 

327 if len(self.Entries) > 1: 

328 if isinstance(self.Entries[-1], Separator): 

329 self.Entries[-1].Show = False 

330 

331 # show_empty tag 

332 for entry in self.Entries[:]: 

333 if isinstance(entry, Menu) and not entry.Layout.show_empty and entry.Visible == 0: 

334 entry.Show = EMPTY 

335 self.Visible -= 1 

336 if entry.NotInXml is True: 

337 self.Entries.remove(entry) 

338 

339 """ PRIVATE STUFF """ 

340 def addSubmenu(self, newmenu): 

341 for submenu in self.Submenus: 

342 if submenu == newmenu: 

343 submenu += newmenu 

344 break 

345 else: 

346 self.Submenus.append(newmenu) 

347 newmenu.Parent = self 

348 newmenu.Depth = self.Depth + 1 

349 

350 # inline tags 

351 def merge_inline(self, submenu): 

352 """Appends a submenu's entries to this menu 

353 See the <Menuname> section of the spec about the "inline" attribute 

354 """ 

355 if len(submenu.Entries) == 1 and submenu.Layout.inline_alias: 

356 menuentry = submenu.Entries[0] 

357 menuentry.DesktopEntry.set("Name", submenu.getName(), locale=True) 

358 menuentry.DesktopEntry.set("GenericName", submenu.getGenericName(), locale=True) 

359 menuentry.DesktopEntry.set("Comment", submenu.getComment(), locale=True) 

360 self.Entries.append(menuentry) 

361 elif len(submenu.Entries) <= submenu.Layout.inline_limit or submenu.Layout.inline_limit == 0: 

362 if submenu.Layout.inline_header: 

363 header = Header(submenu.getName(), submenu.getGenericName(), submenu.getComment()) 

364 self.Entries.append(header) 

365 for entry in submenu.Entries: 

366 self.Entries.append(entry) 

367 else: 

368 self.Entries.append(submenu) 

369 

370 

371class Move: 

372 "A move operation" 

373 def __init__(self, old="", new=""): 

374 self.Old = old 

375 self.New = new 

376 

377 def __cmp__(self, other): 

378 return cmp(self.Old, other.Old) 

379 

380 

381class Layout: 

382 "Menu Layout class" 

383 def __init__(self, show_empty=False, inline=False, inline_limit=4, 

384 inline_header=True, inline_alias=False): 

385 self.show_empty = show_empty 

386 self.inline = inline 

387 self.inline_limit = inline_limit 

388 self.inline_header = inline_header 

389 self.inline_alias = inline_alias 

390 self._order = [] 

391 self._default_order = [ 

392 ['Merge', 'menus'], 

393 ['Merge', 'files'] 

394 ] 

395 

396 @property 

397 def order(self): 

398 return self._order if self._order else self._default_order 

399 

400 @order.setter 

401 def order(self, order): 

402 self._order = order 

403 

404 

405class Rule: 

406 """Include / Exclude Rules Class""" 

407 

408 TYPE_INCLUDE, TYPE_EXCLUDE = 0, 1 

409 

410 @classmethod 

411 def fromFilename(cls, type, filename): 

412 tree = ast.Expression( 

413 body=ast.Compare( 

414 left=ast.Str(filename), 

415 ops=[ast.Eq()], 

416 comparators=[ast.Attribute( 

417 value=ast.Name(id='menuentry', ctx=ast.Load()), 

418 attr='DesktopFileID', 

419 ctx=ast.Load() 

420 )] 

421 ), 

422 lineno=1, col_offset=0 

423 ) 

424 ast.fix_missing_locations(tree) 

425 rule = Rule(type, tree) 

426 return rule 

427 

428 def __init__(self, type, expression): 

429 # Type is TYPE_INCLUDE or TYPE_EXCLUDE 

430 self.Type = type 

431 # expression is ast.Expression 

432 self.expression = expression 

433 self.code = compile(self.expression, '<compiled-menu-rule>', 'eval') 

434 

435 def __str__(self): 

436 return ast.dump(self.expression) 

437 

438 def apply(self, menuentries, run): 

439 for menuentry in menuentries: 

440 if run == 2 and (menuentry.MatchedInclude is True or 

441 menuentry.Allocated is True): 

442 continue 

443 if eval(self.code): 

444 if self.Type is Rule.TYPE_INCLUDE: 

445 menuentry.Add = True 

446 menuentry.MatchedInclude = True 

447 else: 

448 menuentry.Add = False 

449 return menuentries 

450 

451 

452class MenuEntry: 

453 "Wrapper for 'Menu Style' Desktop Entries" 

454 

455 TYPE_USER = "User" 

456 TYPE_SYSTEM = "System" 

457 TYPE_BOTH = "Both" 

458 

459 def __init__(self, filename, dir="", prefix=""): 

460 # Create entry 

461 self.DesktopEntry = DesktopEntry(os.path.join(dir, filename)) 

462 self.setAttributes(filename, dir, prefix) 

463 

464 # Can True, False DELETED, HIDDEN, EMPTY, NOT_SHOW_IN or NO_EXEC 

465 self.Show = True 

466 

467 # Semi-Private 

468 self.Original = None 

469 self.Parents = [] 

470 

471 # Private Stuff 

472 self.Allocated = False 

473 self.Add = False 

474 self.MatchedInclude = False 

475 

476 # Caching 

477 self.Categories = self.DesktopEntry.getCategories() 

478 

479 def save(self): 

480 """Save any changes to the desktop entry.""" 

481 if self.DesktopEntry.tainted: 

482 self.DesktopEntry.write() 

483 

484 def getDir(self): 

485 """Return the directory containing the desktop entry file.""" 

486 return self.DesktopEntry.filename.replace(self.Filename, '') 

487 

488 def getType(self): 

489 """Return the type of MenuEntry, System/User/Both""" 

490 if not xdg.Config.root_mode: 

491 if self.Original: 

492 return self.TYPE_BOTH 

493 elif xdg_data_dirs[0] in self.DesktopEntry.filename: 

494 return self.TYPE_USER 

495 else: 

496 return self.TYPE_SYSTEM 

497 else: 

498 return self.TYPE_USER 

499 

500 def setAttributes(self, filename, dir="", prefix=""): 

501 self.Filename = filename 

502 self.Prefix = prefix 

503 self.DesktopFileID = os.path.join(prefix, filename).replace("/", "-") 

504 

505 if not os.path.isabs(self.DesktopEntry.filename): 

506 self.__setFilename() 

507 

508 def updateAttributes(self): 

509 if self.getType() == self.TYPE_SYSTEM: 

510 self.Original = MenuEntry(self.Filename, self.getDir(), self.Prefix) 

511 self.__setFilename() 

512 

513 def __setFilename(self): 

514 if not xdg.Config.root_mode: 

515 path = xdg_data_dirs[0] 

516 else: 

517 path = xdg_data_dirs[1] 

518 

519 if self.DesktopEntry.getType() == "Application": 

520 dir_ = os.path.join(path, "applications") 

521 else: 

522 dir_ = os.path.join(path, "desktop-directories") 

523 

524 self.DesktopEntry.filename = os.path.join(dir_, self.Filename) 

525 

526 def __cmp__(self, other): 

527 return locale.strcoll(self.DesktopEntry.getName(), other.DesktopEntry.getName()) 

528 

529 def _key(self): 

530 """Key function for locale-aware sorting.""" 

531 return _strxfrm(self.DesktopEntry.getName()) 

532 

533 def __lt__(self, other): 

534 try: 

535 other = other._key() 

536 except AttributeError: 

537 pass 

538 return self._key() < other 

539 

540 def __eq__(self, other): 

541 if self.DesktopFileID == str(other): 

542 return True 

543 else: 

544 return False 

545 

546 def __repr__(self): 

547 return self.DesktopFileID 

548 

549 

550class Separator: 

551 "Just a dummy class for Separators" 

552 def __init__(self, parent): 

553 self.Parent = parent 

554 self.Show = True 

555 

556 

557class Header: 

558 "Class for Inline Headers" 

559 def __init__(self, name, generic_name, comment): 

560 self.Name = name 

561 self.GenericName = generic_name 

562 self.Comment = comment 

563 

564 def __str__(self): 

565 return self.Name 

566 

567 

568TYPE_DIR, TYPE_FILE = 0, 1 

569 

570 

571def _check_file_path(value, filename, type): 

572 path = os.path.dirname(filename) 

573 if not os.path.isabs(value): 

574 value = os.path.join(path, value) 

575 value = os.path.abspath(value) 

576 if not os.path.exists(value): 

577 return False 

578 if type == TYPE_DIR and os.path.isdir(value): 

579 return value 

580 if type == TYPE_FILE and os.path.isfile(value): 

581 return value 

582 return False 

583 

584 

585def _get_menu_file_path(filename): 

586 dirs = list(xdg_config_dirs) 

587 if xdg.Config.root_mode is True: 

588 dirs.pop(0) 

589 for d in dirs: 

590 menuname = os.path.join(d, "menus", filename) 

591 if os.path.isfile(menuname): 

592 return menuname 

593 

594 

595def _to_bool(value): 

596 if isinstance(value, bool): 

597 return value 

598 return value.lower() == "true" 

599 

600 

601# remove duplicate entries from a list 

602def _dedupe(_list): 

603 _set = {} 

604 _list.reverse() 

605 _list = [_set.setdefault(e, e) for e in _list if e not in _set] 

606 _list.reverse() 

607 return _list 

608 

609 

610class XMLMenuBuilder(object): 

611 

612 def __init__(self, debug=False): 

613 self.debug = debug 

614 

615 def parse(self, filename=None): 

616 """Load an applications.menu file. 

617 

618 filename : str, optional 

619 The default is ``$XDG_CONFIG_DIRS/menus/${XDG_MENU_PREFIX}applications.menu``. 

620 """ 

621 # convert to absolute path 

622 if filename and not os.path.isabs(filename): 

623 filename = _get_menu_file_path(filename) 

624 # use default if no filename given 

625 if not filename: 

626 candidate = os.environ.get('XDG_MENU_PREFIX', '') + "applications.menu" 

627 filename = _get_menu_file_path(candidate) 

628 if not filename: 

629 raise ParsingError('File not found', "/etc/xdg/menus/%s" % candidate) 

630 # check if it is a .menu file 

631 if not filename.endswith(".menu"): 

632 raise ParsingError('Not a .menu file', filename) 

633 # create xml parser 

634 try: 

635 tree = etree.parse(filename) 

636 except: 

637 raise ParsingError('Not a valid .menu file', filename) 

638 

639 # parse menufile 

640 self._merged_files = set() 

641 self._directory_dirs = set() 

642 self.cache = MenuEntryCache() 

643 

644 menu = self.parse_menu(tree.getroot(), filename) 

645 menu.tree = tree 

646 menu.filename = filename 

647 

648 self.handle_moves(menu) 

649 self.post_parse(menu) 

650 

651 # generate the menu 

652 self.generate_not_only_allocated(menu) 

653 self.generate_only_allocated(menu) 

654 

655 # and finally sort 

656 menu.sort() 

657 

658 return menu 

659 

660 def parse_menu(self, node, filename): 

661 menu = Menu() 

662 self.parse_node(node, filename, menu) 

663 return menu 

664 

665 def parse_node(self, node, filename, parent=None): 

666 num_children = len(node) 

667 for child in node: 

668 tag, text = child.tag, child.text 

669 text = text.strip() if text else None 

670 if tag == 'Menu': 

671 menu = self.parse_menu(child, filename) 

672 parent.addSubmenu(menu) 

673 elif tag == 'AppDir' and text: 

674 self.parse_app_dir(text, filename, parent) 

675 elif tag == 'DefaultAppDirs': 

676 self.parse_default_app_dir(filename, parent) 

677 elif tag == 'DirectoryDir' and text: 

678 self.parse_directory_dir(text, filename, parent) 

679 elif tag == 'DefaultDirectoryDirs': 

680 self.parse_default_directory_dir(filename, parent) 

681 elif tag == 'Name' and text: 

682 parent.Name = text 

683 elif tag == 'Directory' and text: 

684 parent.Directories.append(text) 

685 elif tag == 'OnlyUnallocated': 

686 parent.OnlyUnallocated = True 

687 elif tag == 'NotOnlyUnallocated': 

688 parent.OnlyUnallocated = False 

689 elif tag == 'Deleted': 

690 parent.Deleted = True 

691 elif tag == 'NotDeleted': 

692 parent.Deleted = False 

693 elif tag == 'Include' or tag == 'Exclude': 

694 parent.Rules.append(self.parse_rule(child)) 

695 elif tag == 'MergeFile': 

696 if child.attrib.get("type", None) == "parent": 

697 self.parse_merge_file("applications.menu", child, filename, parent) 

698 elif text: 

699 self.parse_merge_file(text, child, filename, parent) 

700 elif tag == 'MergeDir' and text: 

701 self.parse_merge_dir(text, child, filename, parent) 

702 elif tag == 'DefaultMergeDirs': 

703 self.parse_default_merge_dirs(child, filename, parent) 

704 elif tag == 'Move': 

705 parent.Moves.append(self.parse_move(child)) 

706 elif tag == 'Layout': 

707 if num_children > 1: 

708 parent.Layout = self.parse_layout(child) 

709 elif tag == 'DefaultLayout': 

710 if num_children > 1: 

711 parent.DefaultLayout = self.parse_layout(child) 

712 elif tag == 'LegacyDir' and text: 

713 self.parse_legacy_dir(text, child.attrib.get("prefix", ""), filename, parent) 

714 elif tag == 'KDELegacyDirs': 

715 self.parse_kde_legacy_dirs(filename, parent) 

716 

717 def parse_layout(self, node): 

718 layout = Layout( 

719 show_empty=_to_bool(node.attrib.get("show_empty", False)), 

720 inline=_to_bool(node.attrib.get("inline", False)), 

721 inline_limit=int(node.attrib.get("inline_limit", 4)), 

722 inline_header=_to_bool(node.attrib.get("inline_header", True)), 

723 inline_alias=_to_bool(node.attrib.get("inline_alias", False)) 

724 ) 

725 order = [] 

726 for child in node: 

727 tag, text = child.tag, child.text 

728 text = text.strip() if text else None 

729 if tag == "Menuname" and text: 

730 order.append([ 

731 "Menuname", 

732 text, 

733 _to_bool(child.attrib.get("show_empty", False)), 

734 _to_bool(child.attrib.get("inline", False)), 

735 int(child.attrib.get("inline_limit", 4)), 

736 _to_bool(child.attrib.get("inline_header", True)), 

737 _to_bool(child.attrib.get("inline_alias", False)) 

738 ]) 

739 elif tag == "Separator": 

740 order.append(['Separator']) 

741 elif tag == "Filename" and text: 

742 order.append(["Filename", text]) 

743 elif tag == "Merge": 

744 order.append([ 

745 "Merge", 

746 child.attrib.get("type", "all") 

747 ]) 

748 layout.order = order 

749 return layout 

750 

751 def parse_move(self, node): 

752 old, new = "", "" 

753 for child in node: 

754 tag, text = child.tag, child.text 

755 text = text.strip() if text else None 

756 if tag == "Old" and text: 

757 old = text 

758 elif tag == "New" and text: 

759 new = text 

760 return Move(old, new) 

761 

762 # ---------- <Rule> parsing 

763 

764 def parse_rule(self, node): 

765 type = Rule.TYPE_INCLUDE if node.tag == 'Include' else Rule.TYPE_EXCLUDE 

766 tree = ast.Expression(lineno=1, col_offset=0) 

767 expr = self.parse_bool_op(node, ast.Or()) 

768 if expr: 

769 tree.body = expr 

770 else: 

771 tree.body = _ast_const('False') 

772 ast.fix_missing_locations(tree) 

773 return Rule(type, tree) 

774 

775 def parse_bool_op(self, node, operator): 

776 values = [] 

777 for child in node: 

778 rule = self.parse_rule_node(child) 

779 if rule: 

780 values.append(rule) 

781 num_values = len(values) 

782 if num_values > 1: 

783 return ast.BoolOp(operator, values) 

784 elif num_values == 1: 

785 return values[0] 

786 return None 

787 

788 def parse_rule_node(self, node): 

789 tag = node.tag 

790 if tag == 'Or': 

791 return self.parse_bool_op(node, ast.Or()) 

792 elif tag == 'And': 

793 return self.parse_bool_op(node, ast.And()) 

794 elif tag == 'Not': 

795 expr = self.parse_bool_op(node, ast.Or()) 

796 return ast.UnaryOp(ast.Not(), expr) if expr else None 

797 elif tag == 'All': 

798 return _ast_const('True') 

799 elif tag == 'Category': 

800 category = node.text 

801 return ast.Compare( 

802 left=ast.Str(category), 

803 ops=[ast.In()], 

804 comparators=[ast.Attribute( 

805 value=ast.Name(id='menuentry', ctx=ast.Load()), 

806 attr='Categories', 

807 ctx=ast.Load() 

808 )] 

809 ) 

810 elif tag == 'Filename': 

811 filename = node.text 

812 return ast.Compare( 

813 left=ast.Str(filename), 

814 ops=[ast.Eq()], 

815 comparators=[ast.Attribute( 

816 value=ast.Name(id='menuentry', ctx=ast.Load()), 

817 attr='DesktopFileID', 

818 ctx=ast.Load() 

819 )] 

820 ) 

821 

822 # ---------- App/Directory Dir Stuff 

823 

824 def parse_app_dir(self, value, filename, parent): 

825 value = _check_file_path(value, filename, TYPE_DIR) 

826 if value: 

827 parent.AppDirs.append(value) 

828 

829 def parse_default_app_dir(self, filename, parent): 

830 for d in reversed(xdg_data_dirs): 

831 self.parse_app_dir(os.path.join(d, "applications"), filename, parent) 

832 

833 def parse_directory_dir(self, value, filename, parent): 

834 value = _check_file_path(value, filename, TYPE_DIR) 

835 if value: 

836 parent.DirectoryDirs.append(value) 

837 

838 def parse_default_directory_dir(self, filename, parent): 

839 for d in reversed(xdg_data_dirs): 

840 self.parse_directory_dir(os.path.join(d, "desktop-directories"), filename, parent) 

841 

842 # ---------- Merge Stuff 

843 

844 def parse_merge_file(self, value, child, filename, parent): 

845 if child.attrib.get("type", None) == "parent": 

846 for d in xdg_config_dirs: 

847 rel_file = filename.replace(d, "").strip("/") 

848 if rel_file != filename: 

849 for p in xdg_config_dirs: 

850 if d == p: 

851 continue 

852 if os.path.isfile(os.path.join(p, rel_file)): 

853 self.merge_file(os.path.join(p, rel_file), child, parent) 

854 break 

855 else: 

856 value = _check_file_path(value, filename, TYPE_FILE) 

857 if value: 

858 self.merge_file(value, child, parent) 

859 

860 def parse_merge_dir(self, value, child, filename, parent): 

861 value = _check_file_path(value, filename, TYPE_DIR) 

862 if value: 

863 for item in os.listdir(value): 

864 try: 

865 if item.endswith(".menu"): 

866 self.merge_file(os.path.join(value, item), child, parent) 

867 except UnicodeDecodeError: 

868 continue 

869 

870 def parse_default_merge_dirs(self, child, filename, parent): 

871 basename = os.path.splitext(os.path.basename(filename))[0] 

872 for d in reversed(xdg_config_dirs): 

873 self.parse_merge_dir(os.path.join(d, "menus", basename + "-merged"), child, filename, parent) 

874 

875 def merge_file(self, filename, child, parent): 

876 # check for infinite loops 

877 if filename in self._merged_files: 

878 if self.debug: 

879 raise ParsingError('Infinite MergeFile loop detected', filename) 

880 else: 

881 return 

882 self._merged_files.add(filename) 

883 # load file 

884 try: 

885 tree = etree.parse(filename) 

886 except IOError: 

887 if self.debug: 

888 raise ParsingError('File not found', filename) 

889 else: 

890 return 

891 except: 

892 if self.debug: 

893 raise ParsingError('Not a valid .menu file', filename) 

894 else: 

895 return 

896 root = tree.getroot() 

897 self.parse_node(root, filename, parent) 

898 

899 # ---------- Legacy Dir Stuff 

900 

901 def parse_legacy_dir(self, dir_, prefix, filename, parent): 

902 m = self.merge_legacy_dir(dir_, prefix, filename, parent) 

903 if m: 

904 parent += m 

905 

906 def merge_legacy_dir(self, dir_, prefix, filename, parent): 

907 dir_ = _check_file_path(dir_, filename, TYPE_DIR) 

908 if dir_ and dir_ not in self._directory_dirs: 

909 self._directory_dirs.add(dir_) 

910 m = Menu() 

911 m.AppDirs.append(dir_) 

912 m.DirectoryDirs.append(dir_) 

913 m.Name = os.path.basename(dir_) 

914 m.NotInXml = True 

915 

916 for item in os.listdir(dir_): 

917 try: 

918 if item == ".directory": 

919 m.Directories.append(item) 

920 elif os.path.isdir(os.path.join(dir_, item)): 

921 m.addSubmenu(self.merge_legacy_dir( 

922 os.path.join(dir_, item), 

923 prefix, 

924 filename, 

925 parent 

926 )) 

927 except UnicodeDecodeError: 

928 continue 

929 

930 self.cache.add_menu_entries([dir_], prefix, True) 

931 menuentries = self.cache.get_menu_entries([dir_], False) 

932 

933 for menuentry in menuentries: 

934 categories = menuentry.Categories 

935 if len(categories) == 0: 

936 r = Rule.fromFilename(Rule.TYPE_INCLUDE, menuentry.DesktopFileID) 

937 m.Rules.append(r) 

938 if not dir_ in parent.AppDirs: 

939 categories.append("Legacy") 

940 menuentry.Categories = categories 

941 

942 return m 

943 

944 def parse_kde_legacy_dirs(self, filename, parent): 

945 try: 

946 proc = subprocess.Popen( 

947 ['kde-config', '--path', 'apps'], 

948 stdout=subprocess.PIPE, 

949 universal_newlines=True 

950 ) 

951 output = proc.communicate()[0].splitlines() 

952 except OSError: 

953 # If kde-config doesn't exist, ignore this. 

954 return 

955 try: 

956 for dir_ in output[0].split(":"): 

957 self.parse_legacy_dir(dir_, "kde", filename, parent) 

958 except IndexError: 

959 pass 

960 

961 def post_parse(self, menu): 

962 # unallocated / deleted 

963 if menu.Deleted is None: 

964 menu.Deleted = False 

965 if menu.OnlyUnallocated is None: 

966 menu.OnlyUnallocated = False 

967 

968 # Layout Tags 

969 if not menu.Layout or not menu.DefaultLayout: 

970 if menu.DefaultLayout: 

971 menu.Layout = menu.DefaultLayout 

972 elif menu.Layout: 

973 if menu.Depth > 0: 

974 menu.DefaultLayout = menu.Parent.DefaultLayout 

975 else: 

976 menu.DefaultLayout = Layout() 

977 else: 

978 if menu.Depth > 0: 

979 menu.Layout = menu.Parent.DefaultLayout 

980 menu.DefaultLayout = menu.Parent.DefaultLayout 

981 else: 

982 menu.Layout = Layout() 

983 menu.DefaultLayout = Layout() 

984 

985 # add parent's app/directory dirs 

986 if menu.Depth > 0: 

987 menu.AppDirs = menu.Parent.AppDirs + menu.AppDirs 

988 menu.DirectoryDirs = menu.Parent.DirectoryDirs + menu.DirectoryDirs 

989 

990 # remove duplicates 

991 menu.Directories = _dedupe(menu.Directories) 

992 menu.DirectoryDirs = _dedupe(menu.DirectoryDirs) 

993 menu.AppDirs = _dedupe(menu.AppDirs) 

994 

995 # go recursive through all menus 

996 for submenu in menu.Submenus: 

997 self.post_parse(submenu) 

998 

999 # reverse so handling is easier 

1000 menu.Directories.reverse() 

1001 menu.DirectoryDirs.reverse() 

1002 menu.AppDirs.reverse() 

1003 

1004 # get the valid .directory file out of the list 

1005 for directory in menu.Directories: 

1006 for dir in menu.DirectoryDirs: 

1007 if os.path.isfile(os.path.join(dir, directory)): 

1008 menuentry = MenuEntry(directory, dir) 

1009 if not menu.Directory: 

1010 menu.Directory = menuentry 

1011 elif menuentry.getType() == MenuEntry.TYPE_SYSTEM: 

1012 if menu.Directory.getType() == MenuEntry.TYPE_USER: 

1013 menu.Directory.Original = menuentry 

1014 if menu.Directory: 

1015 break 

1016 

1017 # Finally generate the menu 

1018 def generate_not_only_allocated(self, menu): 

1019 for submenu in menu.Submenus: 

1020 self.generate_not_only_allocated(submenu) 

1021 

1022 if menu.OnlyUnallocated is False: 

1023 self.cache.add_menu_entries(menu.AppDirs) 

1024 menuentries = [] 

1025 for rule in menu.Rules: 

1026 menuentries = rule.apply(self.cache.get_menu_entries(menu.AppDirs), 1) 

1027 

1028 for menuentry in menuentries: 

1029 if menuentry.Add is True: 

1030 menuentry.Parents.append(menu) 

1031 menuentry.Add = False 

1032 menuentry.Allocated = True 

1033 menu.MenuEntries.append(menuentry) 

1034 

1035 def generate_only_allocated(self, menu): 

1036 for submenu in menu.Submenus: 

1037 self.generate_only_allocated(submenu) 

1038 

1039 if menu.OnlyUnallocated is True: 

1040 self.cache.add_menu_entries(menu.AppDirs) 

1041 menuentries = [] 

1042 for rule in menu.Rules: 

1043 menuentries = rule.apply(self.cache.get_menu_entries(menu.AppDirs), 2) 

1044 for menuentry in menuentries: 

1045 if menuentry.Add is True: 

1046 menuentry.Parents.append(menu) 

1047 # menuentry.Add = False 

1048 # menuentry.Allocated = True 

1049 menu.MenuEntries.append(menuentry) 

1050 

1051 def handle_moves(self, menu): 

1052 for submenu in menu.Submenus: 

1053 self.handle_moves(submenu) 

1054 # parse move operations 

1055 for move in menu.Moves: 

1056 move_from_menu = menu.getMenu(move.Old) 

1057 if move_from_menu: 

1058 # FIXME: this is assigned, but never used... 

1059 move_to_menu = menu.getMenu(move.New) 

1060 

1061 menus = move.New.split("/") 

1062 oldparent = None 

1063 while len(menus) > 0: 

1064 if not oldparent: 

1065 oldparent = menu 

1066 newmenu = oldparent.getMenu(menus[0]) 

1067 if not newmenu: 

1068 newmenu = Menu() 

1069 newmenu.Name = menus[0] 

1070 if len(menus) > 1: 

1071 newmenu.NotInXml = True 

1072 oldparent.addSubmenu(newmenu) 

1073 oldparent = newmenu 

1074 menus.pop(0) 

1075 

1076 newmenu += move_from_menu 

1077 move_from_menu.Parent.Submenus.remove(move_from_menu) 

1078 

1079 

1080class MenuEntryCache: 

1081 "Class to cache Desktop Entries" 

1082 def __init__(self): 

1083 self.cacheEntries = {} 

1084 self.cacheEntries['legacy'] = [] 

1085 self.cache = {} 

1086 

1087 def add_menu_entries(self, dirs, prefix="", legacy=False): 

1088 for dir_ in dirs: 

1089 if not dir_ in self.cacheEntries: 

1090 self.cacheEntries[dir_] = [] 

1091 self.__addFiles(dir_, "", prefix, legacy) 

1092 

1093 def __addFiles(self, dir_, subdir, prefix, legacy): 

1094 for item in os.listdir(os.path.join(dir_, subdir)): 

1095 if item.endswith(".desktop"): 

1096 try: 

1097 menuentry = MenuEntry(os.path.join(subdir, item), dir_, prefix) 

1098 except ParsingError: 

1099 continue 

1100 

1101 self.cacheEntries[dir_].append(menuentry) 

1102 if legacy: 

1103 self.cacheEntries['legacy'].append(menuentry) 

1104 elif os.path.isdir(os.path.join(dir_, subdir, item)) and not legacy: 

1105 self.__addFiles(dir_, os.path.join(subdir, item), prefix, legacy) 

1106 

1107 def get_menu_entries(self, dirs, legacy=True): 

1108 entries = [] 

1109 ids = set() 

1110 # handle legacy items 

1111 appdirs = dirs[:] 

1112 if legacy: 

1113 appdirs.append("legacy") 

1114 # cache the results again 

1115 key = "".join(appdirs) 

1116 try: 

1117 return self.cache[key] 

1118 except KeyError: 

1119 pass 

1120 for dir_ in appdirs: 

1121 for menuentry in self.cacheEntries[dir_]: 

1122 try: 

1123 if menuentry.DesktopFileID not in ids: 

1124 ids.add(menuentry.DesktopFileID) 

1125 entries.append(menuentry) 

1126 elif menuentry.getType() == MenuEntry.TYPE_SYSTEM: 

1127 # FIXME: This is only 99% correct, but still... 

1128 idx = entries.index(menuentry) 

1129 entry = entries[idx] 

1130 if entry.getType() == MenuEntry.TYPE_USER: 

1131 entry.Original = menuentry 

1132 except UnicodeDecodeError: 

1133 continue 

1134 self.cache[key] = entries 

1135 return entries 

1136 

1137 

1138def parse(filename=None, debug=False): 

1139 """Helper function. 

1140 Equivalent to calling xdg.Menu.XMLMenuBuilder().parse(filename) 

1141 """ 

1142 return XMLMenuBuilder(debug).parse(filename)