SolarFM/plugins/thumbnailer/icons/mixins/xdg/Menu.py

1126 lines
38 KiB
Python
Raw Permalink Normal View History

2021-10-10 06:45:55 +00:00
"""
Implementation of the XDG Menu Specification
http://standards.freedesktop.org/menu-spec/
Example code:
from xdg.Menu import parse, Menu, MenuEntry
def print_menu(menu, tab=0):
for submenu in menu.Entries:
if isinstance(submenu, Menu):
print ("\t" * tab) + unicode(submenu)
print_menu(submenu, tab+1)
elif isinstance(submenu, MenuEntry):
print ("\t" * tab) + unicode(submenu.DesktopEntry)
print_menu(parse())
"""
import os
import locale
import subprocess
import ast
try:
import xml.etree.cElementTree as etree
except ImportError:
import xml.etree.ElementTree as etree
from .BaseDirectory import xdg_data_dirs, xdg_config_dirs
from . import DesktopEntry, Locale, Config
from .Exceptions import ParsingError
from .util import PY3
def _strxfrm(s):
"""Wrapper around locale.strxfrm that accepts unicode strings on Python 2.
See Python bug #2481.
"""
if (not PY3) and isinstance(s, unicode):
s = s.encode('utf-8')
return locale.strxfrm(s)
DELETED = "Deleted"
NO_DISPLAY = "NoDisplay"
HIDDEN = "Hidden"
EMPTY = "Empty"
NOT_SHOW_IN = "NotShowIn"
NO_EXEC = "NoExec"
class Menu:
"""Menu containing sub menus under menu.Entries
Contains both Menu and MenuEntry items.
"""
def __init__(self):
# Public stuff
self.Name = ""
self.Directory = None
self.Entries = []
self.Doc = ""
self.Filename = ""
self.Depth = 0
self.Parent = None
self.NotInXml = False
# Can be True, False, DELETED, NO_DISPLAY, HIDDEN, EMPTY or NOT_SHOW_IN
self.Show = True
self.Visible = 0
# Private stuff, only needed for parsing
self.AppDirs = []
self.DefaultLayout = None
self.Deleted = None
self.Directories = []
self.DirectoryDirs = []
self.Layout = None
self.MenuEntries = []
self.Moves = []
self.OnlyUnallocated = None
self.Rules = []
self.Submenus = []
def __str__(self):
return self.Name
def __add__(self, other):
for dir in other.AppDirs:
self.AppDirs.append(dir)
for dir in other.DirectoryDirs:
self.DirectoryDirs.append(dir)
for directory in other.Directories:
self.Directories.append(directory)
if other.Deleted is not None:
self.Deleted = other.Deleted
if other.OnlyUnallocated is not None:
self.OnlyUnallocated = other.OnlyUnallocated
if other.Layout:
self.Layout = other.Layout
if other.DefaultLayout:
self.DefaultLayout = other.DefaultLayout
for rule in other.Rules:
self.Rules.append(rule)
for move in other.Moves:
self.Moves.append(move)
for submenu in other.Submenus:
self.addSubmenu(submenu)
return self
# FIXME: Performance: cache getName()
def __cmp__(self, other):
return locale.strcoll(self.getName(), other.getName())
def _key(self):
"""Key function for locale-aware sorting."""
return _strxfrm(self.getName())
def __lt__(self, other):
try:
other = other._key()
except AttributeError:
pass
return self._key() < other
def __eq__(self, other):
try:
return self.Name == unicode(other)
except NameError: # unicode() becomes str() in Python 3
return self.Name == str(other)
""" PUBLIC STUFF """
def getEntries(self, show_hidden=False):
"""Interator for a list of Entries visible to the user."""
for entry in self.Entries:
if show_hidden:
yield entry
elif entry.Show is True:
yield entry
# FIXME: Add searchEntry/seaqrchMenu function
# search for name/comment/genericname/desktopfileid
# return multiple items
def getMenuEntry(self, desktopfileid, deep=False):
"""Searches for a MenuEntry with a given DesktopFileID."""
for menuentry in self.MenuEntries:
if menuentry.DesktopFileID == desktopfileid:
return menuentry
if deep:
for submenu in self.Submenus:
submenu.getMenuEntry(desktopfileid, deep)
def getMenu(self, path):
"""Searches for a Menu with a given path."""
array = path.split("/", 1)
for submenu in self.Submenus:
if submenu.Name == array[0]:
if len(array) > 1:
return submenu.getMenu(array[1])
else:
return submenu
def getPath(self, org=False, toplevel=False):
"""Returns this menu's path in the menu structure."""
parent = self
names = []
while 1:
if org:
names.append(parent.Name)
else:
names.append(parent.getName())
if parent.Depth > 0:
parent = parent.Parent
else:
break
names.reverse()
path = ""
if not toplevel:
names.pop(0)
for name in names:
path = os.path.join(path, name)
return path
def getName(self):
"""Returns the menu's localised name."""
try:
return self.Directory.DesktopEntry.getName()
except AttributeError:
return self.Name
def getGenericName(self):
"""Returns the menu's generic name."""
try:
return self.Directory.DesktopEntry.getGenericName()
except AttributeError:
return ""
def getComment(self):
"""Returns the menu's comment text."""
try:
return self.Directory.DesktopEntry.getComment()
except AttributeError:
return ""
def getIcon(self):
"""Returns the menu's icon, filename or simple name"""
try:
return self.Directory.DesktopEntry.getIcon()
except AttributeError:
return ""
def sort(self):
self.Entries = []
self.Visible = 0
for submenu in self.Submenus:
submenu.sort()
_submenus = set()
_entries = set()
for order in self.Layout.order:
if order[0] == "Filename":
_entries.add(order[1])
elif order[0] == "Menuname":
_submenus.add(order[1])
for order in self.Layout.order:
if order[0] == "Separator":
separator = Separator(self)
if len(self.Entries) > 0 and isinstance(self.Entries[-1], Separator):
separator.Show = False
self.Entries.append(separator)
elif order[0] == "Filename":
menuentry = self.getMenuEntry(order[1])
if menuentry:
self.Entries.append(menuentry)
elif order[0] == "Menuname":
submenu = self.getMenu(order[1])
if submenu:
if submenu.Layout.inline:
self.merge_inline(submenu)
else:
self.Entries.append(submenu)
elif order[0] == "Merge":
if order[1] == "files" or order[1] == "all":
self.MenuEntries.sort()
for menuentry in self.MenuEntries:
if menuentry.DesktopFileID not in _entries:
self.Entries.append(menuentry)
elif order[1] == "menus" or order[1] == "all":
self.Submenus.sort()
for submenu in self.Submenus:
if submenu.Name not in _submenus:
if submenu.Layout.inline:
self.merge_inline(submenu)
else:
self.Entries.append(submenu)
# getHidden / NoDisplay / OnlyShowIn / NotOnlyShowIn / Deleted / NoExec
for entry in self.Entries:
entry.Show = True
self.Visible += 1
if isinstance(entry, Menu):
if entry.Deleted is True:
entry.Show = DELETED
self.Visible -= 1
elif isinstance(entry.Directory, MenuEntry):
if entry.Directory.DesktopEntry.getNoDisplay():
entry.Show = NO_DISPLAY
self.Visible -= 1
elif entry.Directory.DesktopEntry.getHidden():
entry.Show = HIDDEN
self.Visible -= 1
elif isinstance(entry, MenuEntry):
if entry.DesktopEntry.getNoDisplay():
entry.Show = NO_DISPLAY
self.Visible -= 1
elif entry.DesktopEntry.getHidden():
entry.Show = HIDDEN
self.Visible -= 1
elif entry.DesktopEntry.getTryExec() and not entry.DesktopEntry.findTryExec():
entry.Show = NO_EXEC
self.Visible -= 1
elif xdg.Config.windowmanager:
if (entry.DesktopEntry.OnlyShowIn != [] and (
xdg.Config.windowmanager not in entry.DesktopEntry.OnlyShowIn
)
) or (
xdg.Config.windowmanager in entry.DesktopEntry.NotShowIn
):
entry.Show = NOT_SHOW_IN
self.Visible -= 1
elif isinstance(entry, Separator):
self.Visible -= 1
# remove separators at the beginning and at the end
if len(self.Entries) > 0:
if isinstance(self.Entries[0], Separator):
self.Entries[0].Show = False
if len(self.Entries) > 1:
if isinstance(self.Entries[-1], Separator):
self.Entries[-1].Show = False
# show_empty tag
for entry in self.Entries[:]:
if isinstance(entry, Menu) and not entry.Layout.show_empty and entry.Visible == 0:
entry.Show = EMPTY
self.Visible -= 1
if entry.NotInXml is True:
self.Entries.remove(entry)
""" PRIVATE STUFF """
def addSubmenu(self, newmenu):
for submenu in self.Submenus:
if submenu == newmenu:
submenu += newmenu
break
else:
self.Submenus.append(newmenu)
newmenu.Parent = self
newmenu.Depth = self.Depth + 1
# inline tags
def merge_inline(self, submenu):
"""Appends a submenu's entries to this menu
See the <Menuname> section of the spec about the "inline" attribute
"""
if len(submenu.Entries) == 1 and submenu.Layout.inline_alias:
menuentry = submenu.Entries[0]
menuentry.DesktopEntry.set("Name", submenu.getName(), locale=True)
menuentry.DesktopEntry.set("GenericName", submenu.getGenericName(), locale=True)
menuentry.DesktopEntry.set("Comment", submenu.getComment(), locale=True)
self.Entries.append(menuentry)
elif len(submenu.Entries) <= submenu.Layout.inline_limit or submenu.Layout.inline_limit == 0:
if submenu.Layout.inline_header:
header = Header(submenu.getName(), submenu.getGenericName(), submenu.getComment())
self.Entries.append(header)
for entry in submenu.Entries:
self.Entries.append(entry)
else:
self.Entries.append(submenu)
class Move:
"A move operation"
def __init__(self, old="", new=""):
self.Old = old
self.New = new
def __cmp__(self, other):
return cmp(self.Old, other.Old)
class Layout:
"Menu Layout class"
def __init__(self, show_empty=False, inline=False, inline_limit=4,
inline_header=True, inline_alias=False):
self.show_empty = show_empty
self.inline = inline
self.inline_limit = inline_limit
self.inline_header = inline_header
self.inline_alias = inline_alias
self._order = []
self._default_order = [
['Merge', 'menus'],
['Merge', 'files']
]
@property
def order(self):
return self._order if self._order else self._default_order
@order.setter
def order(self, order):
self._order = order
class Rule:
"""Include / Exclude Rules Class"""
TYPE_INCLUDE, TYPE_EXCLUDE = 0, 1
@classmethod
def fromFilename(cls, type, filename):
tree = ast.Expression(
body=ast.Compare(
left=ast.Str(filename),
ops=[ast.Eq()],
comparators=[ast.Attribute(
value=ast.Name(id='menuentry', ctx=ast.Load()),
attr='DesktopFileID',
ctx=ast.Load()
)]
),
lineno=1, col_offset=0
)
ast.fix_missing_locations(tree)
rule = Rule(type, tree)
return rule
def __init__(self, type, expression):
# Type is TYPE_INCLUDE or TYPE_EXCLUDE
self.Type = type
# expression is ast.Expression
self.expression = expression
self.code = compile(self.expression, '<compiled-menu-rule>', 'eval')
def __str__(self):
return ast.dump(self.expression)
def apply(self, menuentries, run):
for menuentry in menuentries:
if run == 2 and (menuentry.MatchedInclude is True or
menuentry.Allocated is True):
continue
if eval(self.code):
if self.Type is Rule.TYPE_INCLUDE:
menuentry.Add = True
menuentry.MatchedInclude = True
else:
menuentry.Add = False
return menuentries
class MenuEntry:
"Wrapper for 'Menu Style' Desktop Entries"
TYPE_USER = "User"
TYPE_SYSTEM = "System"
TYPE_BOTH = "Both"
def __init__(self, filename, dir="", prefix=""):
# Create entry
self.DesktopEntry = DesktopEntry(os.path.join(dir, filename))
self.setAttributes(filename, dir, prefix)
# Can True, False DELETED, HIDDEN, EMPTY, NOT_SHOW_IN or NO_EXEC
self.Show = True
# Semi-Private
self.Original = None
self.Parents = []
# Private Stuff
self.Allocated = False
self.Add = False
self.MatchedInclude = False
# Caching
self.Categories = self.DesktopEntry.getCategories()
def save(self):
"""Save any changes to the desktop entry."""
if self.DesktopEntry.tainted:
self.DesktopEntry.write()
def getDir(self):
"""Return the directory containing the desktop entry file."""
return self.DesktopEntry.filename.replace(self.Filename, '')
def getType(self):
"""Return the type of MenuEntry, System/User/Both"""
if not xdg.Config.root_mode:
if self.Original:
return self.TYPE_BOTH
elif xdg_data_dirs[0] in self.DesktopEntry.filename:
return self.TYPE_USER
else:
return self.TYPE_SYSTEM
else:
return self.TYPE_USER
def setAttributes(self, filename, dir="", prefix=""):
self.Filename = filename
self.Prefix = prefix
self.DesktopFileID = os.path.join(prefix, filename).replace("/", "-")
if not os.path.isabs(self.DesktopEntry.filename):
self.__setFilename()
def updateAttributes(self):
if self.getType() == self.TYPE_SYSTEM:
self.Original = MenuEntry(self.Filename, self.getDir(), self.Prefix)
self.__setFilename()
def __setFilename(self):
if not xdg.Config.root_mode:
path = xdg_data_dirs[0]
else:
path = xdg_data_dirs[1]
if self.DesktopEntry.getType() == "Application":
dir_ = os.path.join(path, "applications")
else:
dir_ = os.path.join(path, "desktop-directories")
self.DesktopEntry.filename = os.path.join(dir_, self.Filename)
def __cmp__(self, other):
return locale.strcoll(self.DesktopEntry.getName(), other.DesktopEntry.getName())
def _key(self):
"""Key function for locale-aware sorting."""
return _strxfrm(self.DesktopEntry.getName())
def __lt__(self, other):
try:
other = other._key()
except AttributeError:
pass
return self._key() < other
def __eq__(self, other):
if self.DesktopFileID == str(other):
return True
else:
return False
def __repr__(self):
return self.DesktopFileID
class Separator:
"Just a dummy class for Separators"
def __init__(self, parent):
self.Parent = parent
self.Show = True
class Header:
"Class for Inline Headers"
def __init__(self, name, generic_name, comment):
self.Name = name
self.GenericName = generic_name
self.Comment = comment
def __str__(self):
return self.Name
TYPE_DIR, TYPE_FILE = 0, 1
def _check_file_path(value, filename, type):
path = os.path.dirname(filename)
if not os.path.isabs(value):
value = os.path.join(path, value)
value = os.path.abspath(value)
if not os.path.exists(value):
return False
if type == TYPE_DIR and os.path.isdir(value):
return value
if type == TYPE_FILE and os.path.isfile(value):
return value
return False
def _get_menu_file_path(filename):
dirs = list(xdg_config_dirs)
if xdg.Config.root_mode is True:
dirs.pop(0)
for d in dirs:
menuname = os.path.join(d, "menus", filename)
if os.path.isfile(menuname):
return menuname
def _to_bool(value):
if isinstance(value, bool):
return value
return value.lower() == "true"
# remove duplicate entries from a list
def _dedupe(_list):
_set = {}
_list.reverse()
_list = [_set.setdefault(e, e) for e in _list if e not in _set]
_list.reverse()
return _list
class XMLMenuBuilder(object):
def __init__(self, debug=False):
self.debug = debug
def parse(self, filename=None):
"""Load an applications.menu file.
filename : str, optional
The default is ``$XDG_CONFIG_DIRS/menus/${XDG_MENU_PREFIX}applications.menu``.
"""
# convert to absolute path
if filename and not os.path.isabs(filename):
filename = _get_menu_file_path(filename)
# use default if no filename given
if not filename:
candidate = os.environ.get('XDG_MENU_PREFIX', '') + "applications.menu"
filename = _get_menu_file_path(candidate)
if not filename:
raise ParsingError('File not found', "/etc/xdg/menus/%s" % candidate)
# check if it is a .menu file
if not filename.endswith(".menu"):
raise ParsingError('Not a .menu file', filename)
# create xml parser
try:
tree = etree.parse(filename)
except:
raise ParsingError('Not a valid .menu file', filename)
# parse menufile
self._merged_files = set()
self._directory_dirs = set()
self.cache = MenuEntryCache()
menu = self.parse_menu(tree.getroot(), filename)
menu.tree = tree
menu.filename = filename
self.handle_moves(menu)
self.post_parse(menu)
# generate the menu
self.generate_not_only_allocated(menu)
self.generate_only_allocated(menu)
# and finally sort
menu.sort()
return menu
def parse_menu(self, node, filename):
menu = Menu()
self.parse_node(node, filename, menu)
return menu
def parse_node(self, node, filename, parent=None):
num_children = len(node)
for child in node:
tag, text = child.tag, child.text
text = text.strip() if text else None
if tag == 'Menu':
menu = self.parse_menu(child, filename)
parent.addSubmenu(menu)
elif tag == 'AppDir' and text:
self.parse_app_dir(text, filename, parent)
elif tag == 'DefaultAppDirs':
self.parse_default_app_dir(filename, parent)
elif tag == 'DirectoryDir' and text:
self.parse_directory_dir(text, filename, parent)
elif tag == 'DefaultDirectoryDirs':
self.parse_default_directory_dir(filename, parent)
elif tag == 'Name' and text:
parent.Name = text
elif tag == 'Directory' and text:
parent.Directories.append(text)
elif tag == 'OnlyUnallocated':
parent.OnlyUnallocated = True
elif tag == 'NotOnlyUnallocated':
parent.OnlyUnallocated = False
elif tag == 'Deleted':
parent.Deleted = True
elif tag == 'NotDeleted':
parent.Deleted = False
elif tag == 'Include' or tag == 'Exclude':
parent.Rules.append(self.parse_rule(child))
elif tag == 'MergeFile':
if child.attrib.get("type", None) == "parent":
self.parse_merge_file("applications.menu", child, filename, parent)
elif text:
self.parse_merge_file(text, child, filename, parent)
elif tag == 'MergeDir' and text:
self.parse_merge_dir(text, child, filename, parent)
elif tag == 'DefaultMergeDirs':
self.parse_default_merge_dirs(child, filename, parent)
elif tag == 'Move':
parent.Moves.append(self.parse_move(child))
elif tag == 'Layout':
if num_children > 1:
parent.Layout = self.parse_layout(child)
elif tag == 'DefaultLayout':
if num_children > 1:
parent.DefaultLayout = self.parse_layout(child)
elif tag == 'LegacyDir' and text:
self.parse_legacy_dir(text, child.attrib.get("prefix", ""), filename, parent)
elif tag == 'KDELegacyDirs':
self.parse_kde_legacy_dirs(filename, parent)
def parse_layout(self, node):
layout = Layout(
show_empty=_to_bool(node.attrib.get("show_empty", False)),
inline=_to_bool(node.attrib.get("inline", False)),
inline_limit=int(node.attrib.get("inline_limit", 4)),
inline_header=_to_bool(node.attrib.get("inline_header", True)),
inline_alias=_to_bool(node.attrib.get("inline_alias", False))
)
for child in node:
tag, text = child.tag, child.text
text = text.strip() if text else None
if tag == "Menuname" and text:
layout.order.append([
"Menuname",
text,
_to_bool(child.attrib.get("show_empty", False)),
_to_bool(child.attrib.get("inline", False)),
int(child.attrib.get("inline_limit", 4)),
_to_bool(child.attrib.get("inline_header", True)),
_to_bool(child.attrib.get("inline_alias", False))
])
elif tag == "Separator":
layout.order.append(['Separator'])
elif tag == "Filename" and text:
layout.order.append(["Filename", text])
elif tag == "Merge":
layout.order.append([
"Merge",
child.attrib.get("type", "all")
])
return layout
def parse_move(self, node):
old, new = "", ""
for child in node:
tag, text = child.tag, child.text
text = text.strip() if text else None
if tag == "Old" and text:
old = text
elif tag == "New" and text:
new = text
return Move(old, new)
# ---------- <Rule> parsing
def parse_rule(self, node):
type = Rule.TYPE_INCLUDE if node.tag == 'Include' else Rule.TYPE_EXCLUDE
tree = ast.Expression(lineno=1, col_offset=0)
expr = self.parse_bool_op(node, ast.Or())
if expr:
tree.body = expr
else:
tree.body = ast.Name('False', ast.Load())
ast.fix_missing_locations(tree)
return Rule(type, tree)
def parse_bool_op(self, node, operator):
values = []
for child in node:
rule = self.parse_rule_node(child)
if rule:
values.append(rule)
num_values = len(values)
if num_values > 1:
return ast.BoolOp(operator, values)
elif num_values == 1:
return values[0]
return None
def parse_rule_node(self, node):
tag = node.tag
if tag == 'Or':
return self.parse_bool_op(node, ast.Or())
elif tag == 'And':
return self.parse_bool_op(node, ast.And())
elif tag == 'Not':
expr = self.parse_bool_op(node, ast.Or())
return ast.UnaryOp(ast.Not(), expr) if expr else None
elif tag == 'All':
return ast.Name('True', ast.Load())
elif tag == 'Category':
category = node.text
return ast.Compare(
left=ast.Str(category),
ops=[ast.In()],
comparators=[ast.Attribute(
value=ast.Name(id='menuentry', ctx=ast.Load()),
attr='Categories',
ctx=ast.Load()
)]
)
elif tag == 'Filename':
filename = node.text
return ast.Compare(
left=ast.Str(filename),
ops=[ast.Eq()],
comparators=[ast.Attribute(
value=ast.Name(id='menuentry', ctx=ast.Load()),
attr='DesktopFileID',
ctx=ast.Load()
)]
)
# ---------- App/Directory Dir Stuff
def parse_app_dir(self, value, filename, parent):
value = _check_file_path(value, filename, TYPE_DIR)
if value:
parent.AppDirs.append(value)
def parse_default_app_dir(self, filename, parent):
for d in reversed(xdg_data_dirs):
self.parse_app_dir(os.path.join(d, "applications"), filename, parent)
def parse_directory_dir(self, value, filename, parent):
value = _check_file_path(value, filename, TYPE_DIR)
if value:
parent.DirectoryDirs.append(value)
def parse_default_directory_dir(self, filename, parent):
for d in reversed(xdg_data_dirs):
self.parse_directory_dir(os.path.join(d, "desktop-directories"), filename, parent)
# ---------- Merge Stuff
def parse_merge_file(self, value, child, filename, parent):
if child.attrib.get("type", None) == "parent":
for d in xdg_config_dirs:
rel_file = filename.replace(d, "").strip("/")
if rel_file != filename:
for p in xdg_config_dirs:
if d == p:
continue
if os.path.isfile(os.path.join(p, rel_file)):
self.merge_file(os.path.join(p, rel_file), child, parent)
break
else:
value = _check_file_path(value, filename, TYPE_FILE)
if value:
self.merge_file(value, child, parent)
def parse_merge_dir(self, value, child, filename, parent):
value = _check_file_path(value, filename, TYPE_DIR)
if value:
for item in os.listdir(value):
try:
if item.endswith(".menu"):
self.merge_file(os.path.join(value, item), child, parent)
except UnicodeDecodeError:
continue
def parse_default_merge_dirs(self, child, filename, parent):
basename = os.path.splitext(os.path.basename(filename))[0]
for d in reversed(xdg_config_dirs):
self.parse_merge_dir(os.path.join(d, "menus", basename + "-merged"), child, filename, parent)
def merge_file(self, filename, child, parent):
# check for infinite loops
if filename in self._merged_files:
if self.debug:
raise ParsingError('Infinite MergeFile loop detected', filename)
else:
return
self._merged_files.add(filename)
# load file
try:
tree = etree.parse(filename)
except IOError:
if self.debug:
raise ParsingError('File not found', filename)
else:
return
except:
if self.debug:
raise ParsingError('Not a valid .menu file', filename)
else:
return
root = tree.getroot()
self.parse_node(root, filename, parent)
# ---------- Legacy Dir Stuff
def parse_legacy_dir(self, dir_, prefix, filename, parent):
m = self.merge_legacy_dir(dir_, prefix, filename, parent)
if m:
parent += m
def merge_legacy_dir(self, dir_, prefix, filename, parent):
dir_ = _check_file_path(dir_, filename, TYPE_DIR)
if dir_ and dir_ not in self._directory_dirs:
self._directory_dirs.add(dir_)
m = Menu()
m.AppDirs.append(dir_)
m.DirectoryDirs.append(dir_)
m.Name = os.path.basename(dir_)
m.NotInXml = True
for item in os.listdir(dir_):
try:
if item == ".directory":
m.Directories.append(item)
elif os.path.isdir(os.path.join(dir_, item)):
m.addSubmenu(self.merge_legacy_dir(
os.path.join(dir_, item),
prefix,
filename,
parent
))
except UnicodeDecodeError:
continue
self.cache.add_menu_entries([dir_], prefix, True)
menuentries = self.cache.get_menu_entries([dir_], False)
for menuentry in menuentries:
categories = menuentry.Categories
if len(categories) == 0:
r = Rule.fromFilename(Rule.TYPE_INCLUDE, menuentry.DesktopFileID)
m.Rules.append(r)
if not dir_ in parent.AppDirs:
categories.append("Legacy")
menuentry.Categories = categories
return m
def parse_kde_legacy_dirs(self, filename, parent):
try:
proc = subprocess.Popen(
['kde-config', '--path', 'apps'],
stdout=subprocess.PIPE,
universal_newlines=True
)
output = proc.communicate()[0].splitlines()
except OSError:
# If kde-config doesn't exist, ignore this.
return
try:
for dir_ in output[0].split(":"):
self.parse_legacy_dir(dir_, "kde", filename, parent)
except IndexError:
pass
def post_parse(self, menu):
# unallocated / deleted
if menu.Deleted is None:
menu.Deleted = False
if menu.OnlyUnallocated is None:
menu.OnlyUnallocated = False
# Layout Tags
if not menu.Layout or not menu.DefaultLayout:
if menu.DefaultLayout:
menu.Layout = menu.DefaultLayout
elif menu.Layout:
if menu.Depth > 0:
menu.DefaultLayout = menu.Parent.DefaultLayout
else:
menu.DefaultLayout = Layout()
else:
if menu.Depth > 0:
menu.Layout = menu.Parent.DefaultLayout
menu.DefaultLayout = menu.Parent.DefaultLayout
else:
menu.Layout = Layout()
menu.DefaultLayout = Layout()
# add parent's app/directory dirs
if menu.Depth > 0:
menu.AppDirs = menu.Parent.AppDirs + menu.AppDirs
menu.DirectoryDirs = menu.Parent.DirectoryDirs + menu.DirectoryDirs
# remove duplicates
menu.Directories = _dedupe(menu.Directories)
menu.DirectoryDirs = _dedupe(menu.DirectoryDirs)
menu.AppDirs = _dedupe(menu.AppDirs)
# go recursive through all menus
for submenu in menu.Submenus:
self.post_parse(submenu)
# reverse so handling is easier
menu.Directories.reverse()
menu.DirectoryDirs.reverse()
menu.AppDirs.reverse()
# get the valid .directory file out of the list
for directory in menu.Directories:
for dir in menu.DirectoryDirs:
if os.path.isfile(os.path.join(dir, directory)):
menuentry = MenuEntry(directory, dir)
if not menu.Directory:
menu.Directory = menuentry
elif menuentry.Type == MenuEntry.TYPE_SYSTEM:
if menu.Directory.Type == MenuEntry.TYPE_USER:
menu.Directory.Original = menuentry
if menu.Directory:
break
# Finally generate the menu
def generate_not_only_allocated(self, menu):
for submenu in menu.Submenus:
self.generate_not_only_allocated(submenu)
if menu.OnlyUnallocated is False:
self.cache.add_menu_entries(menu.AppDirs)
menuentries = []
for rule in menu.Rules:
menuentries = rule.apply(self.cache.get_menu_entries(menu.AppDirs), 1)
for menuentry in menuentries:
if menuentry.Add is True:
menuentry.Parents.append(menu)
menuentry.Add = False
menuentry.Allocated = True
menu.MenuEntries.append(menuentry)
def generate_only_allocated(self, menu):
for submenu in menu.Submenus:
self.generate_only_allocated(submenu)
if menu.OnlyUnallocated is True:
self.cache.add_menu_entries(menu.AppDirs)
menuentries = []
for rule in menu.Rules:
menuentries = rule.apply(self.cache.get_menu_entries(menu.AppDirs), 2)
for menuentry in menuentries:
if menuentry.Add is True:
menuentry.Parents.append(menu)
# menuentry.Add = False
# menuentry.Allocated = True
menu.MenuEntries.append(menuentry)
def handle_moves(self, menu):
for submenu in menu.Submenus:
self.handle_moves(submenu)
# parse move operations
for move in menu.Moves:
move_from_menu = menu.getMenu(move.Old)
if move_from_menu:
# FIXME: this is assigned, but never used...
move_to_menu = menu.getMenu(move.New)
menus = move.New.split("/")
oldparent = None
while len(menus) > 0:
if not oldparent:
oldparent = menu
newmenu = oldparent.getMenu(menus[0])
if not newmenu:
newmenu = Menu()
newmenu.Name = menus[0]
if len(menus) > 1:
newmenu.NotInXml = True
oldparent.addSubmenu(newmenu)
oldparent = newmenu
menus.pop(0)
newmenu += move_from_menu
move_from_menu.Parent.Submenus.remove(move_from_menu)
class MenuEntryCache:
"Class to cache Desktop Entries"
def __init__(self):
self.cacheEntries = {}
self.cacheEntries['legacy'] = []
self.cache = {}
def add_menu_entries(self, dirs, prefix="", legacy=False):
for dir_ in dirs:
if not dir_ in self.cacheEntries:
self.cacheEntries[dir_] = []
self.__addFiles(dir_, "", prefix, legacy)
def __addFiles(self, dir_, subdir, prefix, legacy):
for item in os.listdir(os.path.join(dir_, subdir)):
if item.endswith(".desktop"):
try:
menuentry = MenuEntry(os.path.join(subdir, item), dir_, prefix)
except ParsingError:
continue
self.cacheEntries[dir_].append(menuentry)
if legacy:
self.cacheEntries['legacy'].append(menuentry)
elif os.path.isdir(os.path.join(dir_, subdir, item)) and not legacy:
self.__addFiles(dir_, os.path.join(subdir, item), prefix, legacy)
def get_menu_entries(self, dirs, legacy=True):
entries = []
ids = set()
# handle legacy items
appdirs = dirs[:]
if legacy:
appdirs.append("legacy")
# cache the results again
key = "".join(appdirs)
try:
return self.cache[key]
except KeyError:
pass
for dir_ in appdirs:
for menuentry in self.cacheEntries[dir_]:
try:
if menuentry.DesktopFileID not in ids:
ids.add(menuentry.DesktopFileID)
entries.append(menuentry)
elif menuentry.getType() == MenuEntry.TYPE_SYSTEM:
# FIXME: This is only 99% correct, but still...
idx = entries.index(menuentry)
entry = entries[idx]
if entry.getType() == MenuEntry.TYPE_USER:
entry.Original = menuentry
except UnicodeDecodeError:
continue
self.cache[key] = entries
return entries
def parse(filename=None, debug=False):
"""Helper function.
Equivalent to calling xdg.Menu.XMLMenuBuilder().parse(filename)
"""
return XMLMenuBuilder(debug).parse(filename)