You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1135 lines
38 KiB
1135 lines
38 KiB
6 years ago
|
"""
|
||
|
Implementation of the XDG Menu Specification Version 1.0.draft-1
|
||
|
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 locale, os, xml.dom.minidom
|
||
|
import subprocess
|
||
|
|
||
|
from xdg.BaseDirectory import xdg_data_dirs, xdg_config_dirs
|
||
|
from xdg.DesktopEntry import DesktopEntry
|
||
|
from xdg.Exceptions import ParsingError, ValidationError, debug
|
||
|
from xdg.util import PY3
|
||
|
|
||
|
import xdg.Locale
|
||
|
import xdg.Config
|
||
|
|
||
|
ELEMENT_NODE = xml.dom.Node.ELEMENT_NODE
|
||
|
|
||
|
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)
|
||
|
|
||
|
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 one of Deleted/NoDisplay/Hidden/Empty/NotShowIn or True
|
||
|
self.Show = True
|
||
|
self.Visible = 0
|
||
|
|
||
|
# Private stuff, only needed for parsing
|
||
|
self.AppDirs = []
|
||
|
self.DefaultLayout = None
|
||
|
self.Deleted = "notset"
|
||
|
self.Directories = []
|
||
|
self.DirectoryDirs = []
|
||
|
self.Layout = None
|
||
|
self.MenuEntries = []
|
||
|
self.Moves = []
|
||
|
self.OnlyUnallocated = "notset"
|
||
|
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 != "notset":
|
||
|
self.Deleted = other.Deleted
|
||
|
|
||
|
if other.OnlyUnallocated != "notset":
|
||
|
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, hidden=False):
|
||
|
"""Interator for a list of Entries visible to the user."""
|
||
|
for entry in self.Entries:
|
||
|
if hidden == True:
|
||
|
yield entry
|
||
|
elif entry.Show == True:
|
||
|
yield entry
|
||
|
|
||
|
# FIXME: Add searchEntry/seaqrchMenu function
|
||
|
# search for name/comment/genericname/desktopfileide
|
||
|
# 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 == True:
|
||
|
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 toplevel == False:
|
||
|
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 ""
|
||
|
|
||
|
""" 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
|
||
|
|
||
|
class Move:
|
||
|
"A move operation"
|
||
|
def __init__(self, node=None):
|
||
|
if node:
|
||
|
self.parseNode(node)
|
||
|
else:
|
||
|
self.Old = ""
|
||
|
self.New = ""
|
||
|
|
||
|
def __cmp__(self, other):
|
||
|
return cmp(self.Old, other.Old)
|
||
|
|
||
|
def parseNode(self, node):
|
||
|
for child in node.childNodes:
|
||
|
if child.nodeType == ELEMENT_NODE:
|
||
|
if child.tagName == "Old":
|
||
|
try:
|
||
|
self.parseOld(child.childNodes[0].nodeValue)
|
||
|
except IndexError:
|
||
|
raise ValidationError('Old cannot be empty', '??')
|
||
|
elif child.tagName == "New":
|
||
|
try:
|
||
|
self.parseNew(child.childNodes[0].nodeValue)
|
||
|
except IndexError:
|
||
|
raise ValidationError('New cannot be empty', '??')
|
||
|
|
||
|
def parseOld(self, value):
|
||
|
self.Old = value
|
||
|
def parseNew(self, value):
|
||
|
self.New = value
|
||
|
|
||
|
|
||
|
class Layout:
|
||
|
"Menu Layout class"
|
||
|
def __init__(self, node=None):
|
||
|
self.order = []
|
||
|
if node:
|
||
|
self.show_empty = node.getAttribute("show_empty") or "false"
|
||
|
self.inline = node.getAttribute("inline") or "false"
|
||
|
self.inline_limit = node.getAttribute("inline_limit") or 4
|
||
|
self.inline_header = node.getAttribute("inline_header") or "true"
|
||
|
self.inline_alias = node.getAttribute("inline_alias") or "false"
|
||
|
self.inline_limit = int(self.inline_limit)
|
||
|
self.parseNode(node)
|
||
|
else:
|
||
|
self.show_empty = "false"
|
||
|
self.inline = "false"
|
||
|
self.inline_limit = 4
|
||
|
self.inline_header = "true"
|
||
|
self.inline_alias = "false"
|
||
|
self.order.append(["Merge", "menus"])
|
||
|
self.order.append(["Merge", "files"])
|
||
|
|
||
|
def parseNode(self, node):
|
||
|
for child in node.childNodes:
|
||
|
if child.nodeType == ELEMENT_NODE:
|
||
|
if child.tagName == "Menuname":
|
||
|
try:
|
||
|
self.parseMenuname(
|
||
|
child.childNodes[0].nodeValue,
|
||
|
child.getAttribute("show_empty") or "false",
|
||
|
child.getAttribute("inline") or "false",
|
||
|
child.getAttribute("inline_limit") or 4,
|
||
|
child.getAttribute("inline_header") or "true",
|
||
|
child.getAttribute("inline_alias") or "false" )
|
||
|
except IndexError:
|
||
|
raise ValidationError('Menuname cannot be empty', "")
|
||
|
elif child.tagName == "Separator":
|
||
|
self.parseSeparator()
|
||
|
elif child.tagName == "Filename":
|
||
|
try:
|
||
|
self.parseFilename(child.childNodes[0].nodeValue)
|
||
|
except IndexError:
|
||
|
raise ValidationError('Filename cannot be empty', "")
|
||
|
elif child.tagName == "Merge":
|
||
|
self.parseMerge(child.getAttribute("type") or "all")
|
||
|
|
||
|
def parseMenuname(self, value, empty="false", inline="false", inline_limit=4, inline_header="true", inline_alias="false"):
|
||
|
self.order.append(["Menuname", value, empty, inline, inline_limit, inline_header, inline_alias])
|
||
|
self.order[-1][4] = int(self.order[-1][4])
|
||
|
|
||
|
def parseSeparator(self):
|
||
|
self.order.append(["Separator"])
|
||
|
|
||
|
def parseFilename(self, value):
|
||
|
self.order.append(["Filename", value])
|
||
|
|
||
|
def parseMerge(self, type="all"):
|
||
|
self.order.append(["Merge", type])
|
||
|
|
||
|
|
||
|
class Rule:
|
||
|
"Inlcude / Exclude Rules Class"
|
||
|
def __init__(self, type, node=None):
|
||
|
# Type is Include or Exclude
|
||
|
self.Type = type
|
||
|
# Rule is a python expression
|
||
|
self.Rule = ""
|
||
|
|
||
|
# Private attributes, only needed for parsing
|
||
|
self.Depth = 0
|
||
|
self.Expr = [ "or" ]
|
||
|
self.New = True
|
||
|
|
||
|
# Begin parsing
|
||
|
if node:
|
||
|
self.parseNode(node)
|
||
|
|
||
|
def __str__(self):
|
||
|
return self.Rule
|
||
|
|
||
|
def do(self, menuentries, type, run):
|
||
|
for menuentry in menuentries:
|
||
|
if run == 2 and ( menuentry.MatchedInclude == True \
|
||
|
or menuentry.Allocated == True ):
|
||
|
continue
|
||
|
elif eval(self.Rule):
|
||
|
if type == "Include":
|
||
|
menuentry.Add = True
|
||
|
menuentry.MatchedInclude = True
|
||
|
else:
|
||
|
menuentry.Add = False
|
||
|
return menuentries
|
||
|
|
||
|
def parseNode(self, node):
|
||
|
for child in node.childNodes:
|
||
|
if child.nodeType == ELEMENT_NODE:
|
||
|
if child.tagName == 'Filename':
|
||
|
try:
|
||
|
self.parseFilename(child.childNodes[0].nodeValue)
|
||
|
except IndexError:
|
||
|
raise ValidationError('Filename cannot be empty', "???")
|
||
|
elif child.tagName == 'Category':
|
||
|
try:
|
||
|
self.parseCategory(child.childNodes[0].nodeValue)
|
||
|
except IndexError:
|
||
|
raise ValidationError('Category cannot be empty', "???")
|
||
|
elif child.tagName == 'All':
|
||
|
self.parseAll()
|
||
|
elif child.tagName == 'And':
|
||
|
self.parseAnd(child)
|
||
|
elif child.tagName == 'Or':
|
||
|
self.parseOr(child)
|
||
|
elif child.tagName == 'Not':
|
||
|
self.parseNot(child)
|
||
|
|
||
|
def parseNew(self, set=True):
|
||
|
if not self.New:
|
||
|
self.Rule += " " + self.Expr[self.Depth] + " "
|
||
|
if not set:
|
||
|
self.New = True
|
||
|
elif set:
|
||
|
self.New = False
|
||
|
|
||
|
def parseFilename(self, value):
|
||
|
self.parseNew()
|
||
|
self.Rule += "menuentry.DesktopFileID == '%s'" % value.strip().replace("\\", r"\\").replace("'", r"\'")
|
||
|
|
||
|
def parseCategory(self, value):
|
||
|
self.parseNew()
|
||
|
self.Rule += "'%s' in menuentry.Categories" % value.strip()
|
||
|
|
||
|
def parseAll(self):
|
||
|
self.parseNew()
|
||
|
self.Rule += "True"
|
||
|
|
||
|
def parseAnd(self, node):
|
||
|
self.parseNew(False)
|
||
|
self.Rule += "("
|
||
|
self.Depth += 1
|
||
|
self.Expr.append("and")
|
||
|
self.parseNode(node)
|
||
|
self.Depth -= 1
|
||
|
self.Expr.pop()
|
||
|
self.Rule += ")"
|
||
|
|
||
|
def parseOr(self, node):
|
||
|
self.parseNew(False)
|
||
|
self.Rule += "("
|
||
|
self.Depth += 1
|
||
|
self.Expr.append("or")
|
||
|
self.parseNode(node)
|
||
|
self.Depth -= 1
|
||
|
self.Expr.pop()
|
||
|
self.Rule += ")"
|
||
|
|
||
|
def parseNot(self, node):
|
||
|
self.parseNew(False)
|
||
|
self.Rule += "not ("
|
||
|
self.Depth += 1
|
||
|
self.Expr.append("or")
|
||
|
self.parseNode(node)
|
||
|
self.Depth -= 1
|
||
|
self.Expr.pop()
|
||
|
self.Rule += ")"
|
||
|
|
||
|
|
||
|
class MenuEntry:
|
||
|
"Wrapper for 'Menu Style' Desktop Entries"
|
||
|
def __init__(self, filename, dir="", prefix=""):
|
||
|
# Create entry
|
||
|
self.DesktopEntry = DesktopEntry(os.path.join(dir,filename))
|
||
|
self.setAttributes(filename, dir, prefix)
|
||
|
|
||
|
# Can be one of Deleted/Hidden/Empty/NotShowIn/NoExec or True
|
||
|
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 == True:
|
||
|
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 xdg.Config.root_mode == False:
|
||
|
if self.Original:
|
||
|
return "Both"
|
||
|
elif xdg_data_dirs[0] in self.DesktopEntry.filename:
|
||
|
return "User"
|
||
|
else:
|
||
|
return "System"
|
||
|
else:
|
||
|
return "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() == "System":
|
||
|
self.Original = MenuEntry(self.Filename, self.getDir(), self.Prefix)
|
||
|
self.__setFilename()
|
||
|
|
||
|
def __setFilename(self):
|
||
|
if xdg.Config.root_mode == False:
|
||
|
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
|
||
|
|
||
|
|
||
|
tmp = {}
|
||
|
|
||
|
def __getFileName(filename):
|
||
|
dirs = xdg_config_dirs[:]
|
||
|
if xdg.Config.root_mode == True:
|
||
|
dirs.pop(0)
|
||
|
|
||
|
for dir in dirs:
|
||
|
menuname = os.path.join (dir, "menus" , filename)
|
||
|
if os.path.isdir(dir) and os.path.isfile(menuname):
|
||
|
return menuname
|
||
|
|
||
|
def parse(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 = __getFileName(filename)
|
||
|
|
||
|
# use default if no filename given
|
||
|
if not filename:
|
||
|
candidate = os.environ.get('XDG_MENU_PREFIX', '') + "applications.menu"
|
||
|
filename = __getFileName(candidate)
|
||
|
|
||
|
if not filename:
|
||
|
raise ParsingError('File not found', "/etc/xdg/menus/%s" % candidate)
|
||
|
|
||
|
# check if it is a .menu file
|
||
|
if not os.path.splitext(filename)[1] == ".menu":
|
||
|
raise ParsingError('Not a .menu file', filename)
|
||
|
|
||
|
# create xml parser
|
||
|
try:
|
||
|
doc = xml.dom.minidom.parse(filename)
|
||
|
except xml.parsers.expat.ExpatError:
|
||
|
raise ParsingError('Not a valid .menu file', filename)
|
||
|
|
||
|
# parse menufile
|
||
|
tmp["Root"] = ""
|
||
|
tmp["mergeFiles"] = []
|
||
|
tmp["DirectoryDirs"] = []
|
||
|
tmp["cache"] = MenuEntryCache()
|
||
|
|
||
|
__parse(doc, filename, tmp["Root"])
|
||
|
__parsemove(tmp["Root"])
|
||
|
__postparse(tmp["Root"])
|
||
|
|
||
|
tmp["Root"].Doc = doc
|
||
|
tmp["Root"].Filename = filename
|
||
|
|
||
|
# generate the menu
|
||
|
__genmenuNotOnlyAllocated(tmp["Root"])
|
||
|
__genmenuOnlyAllocated(tmp["Root"])
|
||
|
|
||
|
# and finally sort
|
||
|
sort(tmp["Root"])
|
||
|
|
||
|
return tmp["Root"]
|
||
|
|
||
|
|
||
|
def __parse(node, filename, parent=None):
|
||
|
for child in node.childNodes:
|
||
|
if child.nodeType == ELEMENT_NODE:
|
||
|
if child.tagName == 'Menu':
|
||
|
__parseMenu(child, filename, parent)
|
||
|
elif child.tagName == 'AppDir':
|
||
|
try:
|
||
|
__parseAppDir(child.childNodes[0].nodeValue, filename, parent)
|
||
|
except IndexError:
|
||
|
raise ValidationError('AppDir cannot be empty', filename)
|
||
|
elif child.tagName == 'DefaultAppDirs':
|
||
|
__parseDefaultAppDir(filename, parent)
|
||
|
elif child.tagName == 'DirectoryDir':
|
||
|
try:
|
||
|
__parseDirectoryDir(child.childNodes[0].nodeValue, filename, parent)
|
||
|
except IndexError:
|
||
|
raise ValidationError('DirectoryDir cannot be empty', filename)
|
||
|
elif child.tagName == 'DefaultDirectoryDirs':
|
||
|
__parseDefaultDirectoryDir(filename, parent)
|
||
|
elif child.tagName == 'Name' :
|
||
|
try:
|
||
|
parent.Name = child.childNodes[0].nodeValue
|
||
|
except IndexError:
|
||
|
raise ValidationError('Name cannot be empty', filename)
|
||
|
elif child.tagName == 'Directory' :
|
||
|
try:
|
||
|
parent.Directories.append(child.childNodes[0].nodeValue)
|
||
|
except IndexError:
|
||
|
raise ValidationError('Directory cannot be empty', filename)
|
||
|
elif child.tagName == 'OnlyUnallocated':
|
||
|
parent.OnlyUnallocated = True
|
||
|
elif child.tagName == 'NotOnlyUnallocated':
|
||
|
parent.OnlyUnallocated = False
|
||
|
elif child.tagName == 'Deleted':
|
||
|
parent.Deleted = True
|
||
|
elif child.tagName == 'NotDeleted':
|
||
|
parent.Deleted = False
|
||
|
elif child.tagName == 'Include' or child.tagName == 'Exclude':
|
||
|
parent.Rules.append(Rule(child.tagName, child))
|
||
|
elif child.tagName == 'MergeFile':
|
||
|
try:
|
||
|
if child.getAttribute("type") == "parent":
|
||
|
__parseMergeFile("applications.menu", child, filename, parent)
|
||
|
else:
|
||
|
__parseMergeFile(child.childNodes[0].nodeValue, child, filename, parent)
|
||
|
except IndexError:
|
||
|
raise ValidationError('MergeFile cannot be empty', filename)
|
||
|
elif child.tagName == 'MergeDir':
|
||
|
try:
|
||
|
__parseMergeDir(child.childNodes[0].nodeValue, child, filename, parent)
|
||
|
except IndexError:
|
||
|
raise ValidationError('MergeDir cannot be empty', filename)
|
||
|
elif child.tagName == 'DefaultMergeDirs':
|
||
|
__parseDefaultMergeDirs(child, filename, parent)
|
||
|
elif child.tagName == 'Move':
|
||
|
parent.Moves.append(Move(child))
|
||
|
elif child.tagName == 'Layout':
|
||
|
if len(child.childNodes) > 1:
|
||
|
parent.Layout = Layout(child)
|
||
|
elif child.tagName == 'DefaultLayout':
|
||
|
if len(child.childNodes) > 1:
|
||
|
parent.DefaultLayout = Layout(child)
|
||
|
elif child.tagName == 'LegacyDir':
|
||
|
try:
|
||
|
__parseLegacyDir(child.childNodes[0].nodeValue, child.getAttribute("prefix"), filename, parent)
|
||
|
except IndexError:
|
||
|
raise ValidationError('LegacyDir cannot be empty', filename)
|
||
|
elif child.tagName == 'KDELegacyDirs':
|
||
|
__parseKDELegacyDirs(filename, parent)
|
||
|
|
||
|
def __parsemove(menu):
|
||
|
for submenu in menu.Submenus:
|
||
|
__parsemove(submenu)
|
||
|
|
||
|
# parse move operations
|
||
|
for move in menu.Moves:
|
||
|
move_from_menu = menu.getMenu(move.Old)
|
||
|
if move_from_menu:
|
||
|
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)
|
||
|
|
||
|
def __postparse(menu):
|
||
|
# unallocated / deleted
|
||
|
if menu.Deleted == "notset":
|
||
|
menu.Deleted = False
|
||
|
if menu.OnlyUnallocated == "notset":
|
||
|
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 = __removeDuplicates(menu.Directories)
|
||
|
menu.DirectoryDirs = __removeDuplicates(menu.DirectoryDirs)
|
||
|
menu.AppDirs = __removeDuplicates(menu.AppDirs)
|
||
|
|
||
|
# go recursive through all menus
|
||
|
for submenu in menu.Submenus:
|
||
|
__postparse(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.getType() == "System":
|
||
|
if menu.Directory.getType() == "User":
|
||
|
menu.Directory.Original = menuentry
|
||
|
if menu.Directory:
|
||
|
break
|
||
|
|
||
|
|
||
|
# Menu parsing stuff
|
||
|
def __parseMenu(child, filename, parent):
|
||
|
m = Menu()
|
||
|
__parse(child, filename, m)
|
||
|
if parent:
|
||
|
parent.addSubmenu(m)
|
||
|
else:
|
||
|
tmp["Root"] = m
|
||
|
|
||
|
# helper function
|
||
|
def __check(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 type == "dir" and os.path.exists(value) and os.path.isdir(value):
|
||
|
return value
|
||
|
elif type == "file" and os.path.exists(value) and os.path.isfile(value):
|
||
|
return value
|
||
|
else:
|
||
|
return False
|
||
|
|
||
|
# App/Directory Dir Stuff
|
||
|
def __parseAppDir(value, filename, parent):
|
||
|
value = __check(value, filename, "dir")
|
||
|
if value:
|
||
|
parent.AppDirs.append(value)
|
||
|
|
||
|
def __parseDefaultAppDir(filename, parent):
|
||
|
for dir in reversed(xdg_data_dirs):
|
||
|
__parseAppDir(os.path.join(dir, "applications"), filename, parent)
|
||
|
|
||
|
def __parseDirectoryDir(value, filename, parent):
|
||
|
value = __check(value, filename, "dir")
|
||
|
if value:
|
||
|
parent.DirectoryDirs.append(value)
|
||
|
|
||
|
def __parseDefaultDirectoryDir(filename, parent):
|
||
|
for dir in reversed(xdg_data_dirs):
|
||
|
__parseDirectoryDir(os.path.join(dir, "desktop-directories"), filename, parent)
|
||
|
|
||
|
# Merge Stuff
|
||
|
def __parseMergeFile(value, child, filename, parent):
|
||
|
if child.getAttribute("type") == "parent":
|
||
|
for dir in xdg_config_dirs:
|
||
|
rel_file = filename.replace(dir, "").strip("/")
|
||
|
if rel_file != filename:
|
||
|
for p in xdg_config_dirs:
|
||
|
if dir == p:
|
||
|
continue
|
||
|
if os.path.isfile(os.path.join(p,rel_file)):
|
||
|
__mergeFile(os.path.join(p,rel_file),child,parent)
|
||
|
break
|
||
|
else:
|
||
|
value = __check(value, filename, "file")
|
||
|
if value:
|
||
|
__mergeFile(value, child, parent)
|
||
|
|
||
|
def __parseMergeDir(value, child, filename, parent):
|
||
|
value = __check(value, filename, "dir")
|
||
|
if value:
|
||
|
for item in os.listdir(value):
|
||
|
try:
|
||
|
if os.path.splitext(item)[1] == ".menu":
|
||
|
__mergeFile(os.path.join(value, item), child, parent)
|
||
|
except UnicodeDecodeError:
|
||
|
continue
|
||
|
|
||
|
def __parseDefaultMergeDirs(child, filename, parent):
|
||
|
basename = os.path.splitext(os.path.basename(filename))[0]
|
||
|
for dir in reversed(xdg_config_dirs):
|
||
|
__parseMergeDir(os.path.join(dir, "menus", basename + "-merged"), child, filename, parent)
|
||
|
|
||
|
def __mergeFile(filename, child, parent):
|
||
|
# check for infinite loops
|
||
|
if filename in tmp["mergeFiles"]:
|
||
|
if debug:
|
||
|
raise ParsingError('Infinite MergeFile loop detected', filename)
|
||
|
else:
|
||
|
return
|
||
|
|
||
|
tmp["mergeFiles"].append(filename)
|
||
|
|
||
|
# load file
|
||
|
try:
|
||
|
doc = xml.dom.minidom.parse(filename)
|
||
|
except IOError:
|
||
|
if debug:
|
||
|
raise ParsingError('File not found', filename)
|
||
|
else:
|
||
|
return
|
||
|
except xml.parsers.expat.ExpatError:
|
||
|
if debug:
|
||
|
raise ParsingError('Not a valid .menu file', filename)
|
||
|
else:
|
||
|
return
|
||
|
|
||
|
# append file
|
||
|
for child in doc.childNodes:
|
||
|
if child.nodeType == ELEMENT_NODE:
|
||
|
__parse(child,filename,parent)
|
||
|
break
|
||
|
|
||
|
# Legacy Dir Stuff
|
||
|
def __parseLegacyDir(dir, prefix, filename, parent):
|
||
|
m = __mergeLegacyDir(dir,prefix,filename,parent)
|
||
|
if m:
|
||
|
parent += m
|
||
|
|
||
|
def __mergeLegacyDir(dir, prefix, filename, parent):
|
||
|
dir = __check(dir,filename,"dir")
|
||
|
if dir and dir not in tmp["DirectoryDirs"]:
|
||
|
tmp["DirectoryDirs"].append(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(__mergeLegacyDir(os.path.join(dir,item), prefix, filename, parent))
|
||
|
except UnicodeDecodeError:
|
||
|
continue
|
||
|
|
||
|
tmp["cache"].addMenuEntries([dir],prefix, True)
|
||
|
menuentries = tmp["cache"].getMenuEntries([dir], False)
|
||
|
|
||
|
for menuentry in menuentries:
|
||
|
categories = menuentry.Categories
|
||
|
if len(categories) == 0:
|
||
|
r = Rule("Include")
|
||
|
r.parseFilename(menuentry.DesktopFileID)
|
||
|
m.Rules.append(r)
|
||
|
if not dir in parent.AppDirs:
|
||
|
categories.append("Legacy")
|
||
|
menuentry.Categories = categories
|
||
|
|
||
|
return m
|
||
|
|
||
|
def __parseKDELegacyDirs(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(":"):
|
||
|
__parseLegacyDir(dir,"kde", filename, parent)
|
||
|
except IndexError:
|
||
|
pass
|
||
|
|
||
|
# remove duplicate entries from a list
|
||
|
def __removeDuplicates(list):
|
||
|
set = {}
|
||
|
list.reverse()
|
||
|
list = [set.setdefault(e,e) for e in list if e not in set]
|
||
|
list.reverse()
|
||
|
return list
|
||
|
|
||
|
# Finally generate the menu
|
||
|
def __genmenuNotOnlyAllocated(menu):
|
||
|
for submenu in menu.Submenus:
|
||
|
__genmenuNotOnlyAllocated(submenu)
|
||
|
|
||
|
if menu.OnlyUnallocated == False:
|
||
|
tmp["cache"].addMenuEntries(menu.AppDirs)
|
||
|
menuentries = []
|
||
|
for rule in menu.Rules:
|
||
|
menuentries = rule.do(tmp["cache"].getMenuEntries(menu.AppDirs), rule.Type, 1)
|
||
|
for menuentry in menuentries:
|
||
|
if menuentry.Add == True:
|
||
|
menuentry.Parents.append(menu)
|
||
|
menuentry.Add = False
|
||
|
menuentry.Allocated = True
|
||
|
menu.MenuEntries.append(menuentry)
|
||
|
|
||
|
def __genmenuOnlyAllocated(menu):
|
||
|
for submenu in menu.Submenus:
|
||
|
__genmenuOnlyAllocated(submenu)
|
||
|
|
||
|
if menu.OnlyUnallocated == True:
|
||
|
tmp["cache"].addMenuEntries(menu.AppDirs)
|
||
|
menuentries = []
|
||
|
for rule in menu.Rules:
|
||
|
menuentries = rule.do(tmp["cache"].getMenuEntries(menu.AppDirs), rule.Type, 2)
|
||
|
for menuentry in menuentries:
|
||
|
if menuentry.Add == True:
|
||
|
menuentry.Parents.append(menu)
|
||
|
# menuentry.Add = False
|
||
|
# menuentry.Allocated = True
|
||
|
menu.MenuEntries.append(menuentry)
|
||
|
|
||
|
# And sorting ...
|
||
|
def sort(menu):
|
||
|
menu.Entries = []
|
||
|
menu.Visible = 0
|
||
|
|
||
|
for submenu in menu.Submenus:
|
||
|
sort(submenu)
|
||
|
|
||
|
tmp_s = []
|
||
|
tmp_e = []
|
||
|
|
||
|
for order in menu.Layout.order:
|
||
|
if order[0] == "Filename":
|
||
|
tmp_e.append(order[1])
|
||
|
elif order[0] == "Menuname":
|
||
|
tmp_s.append(order[1])
|
||
|
|
||
|
for order in menu.Layout.order:
|
||
|
if order[0] == "Separator":
|
||
|
separator = Separator(menu)
|
||
|
if len(menu.Entries) > 0 and isinstance(menu.Entries[-1], Separator):
|
||
|
separator.Show = False
|
||
|
menu.Entries.append(separator)
|
||
|
elif order[0] == "Filename":
|
||
|
menuentry = menu.getMenuEntry(order[1])
|
||
|
if menuentry:
|
||
|
menu.Entries.append(menuentry)
|
||
|
elif order[0] == "Menuname":
|
||
|
submenu = menu.getMenu(order[1])
|
||
|
if submenu:
|
||
|
__parse_inline(submenu, menu)
|
||
|
elif order[0] == "Merge":
|
||
|
if order[1] == "files" or order[1] == "all":
|
||
|
menu.MenuEntries.sort()
|
||
|
for menuentry in menu.MenuEntries:
|
||
|
if menuentry not in tmp_e:
|
||
|
menu.Entries.append(menuentry)
|
||
|
elif order[1] == "menus" or order[1] == "all":
|
||
|
menu.Submenus.sort()
|
||
|
for submenu in menu.Submenus:
|
||
|
if submenu.Name not in tmp_s:
|
||
|
__parse_inline(submenu, menu)
|
||
|
|
||
|
# getHidden / NoDisplay / OnlyShowIn / NotOnlyShowIn / Deleted / NoExec
|
||
|
for entry in menu.Entries:
|
||
|
entry.Show = True
|
||
|
menu.Visible += 1
|
||
|
if isinstance(entry, Menu):
|
||
|
if entry.Deleted == True:
|
||
|
entry.Show = "Deleted"
|
||
|
menu.Visible -= 1
|
||
|
elif isinstance(entry.Directory, MenuEntry):
|
||
|
if entry.Directory.DesktopEntry.getNoDisplay() == True:
|
||
|
entry.Show = "NoDisplay"
|
||
|
menu.Visible -= 1
|
||
|
elif entry.Directory.DesktopEntry.getHidden() == True:
|
||
|
entry.Show = "Hidden"
|
||
|
menu.Visible -= 1
|
||
|
elif isinstance(entry, MenuEntry):
|
||
|
if entry.DesktopEntry.getNoDisplay() == True:
|
||
|
entry.Show = "NoDisplay"
|
||
|
menu.Visible -= 1
|
||
|
elif entry.DesktopEntry.getHidden() == True:
|
||
|
entry.Show = "Hidden"
|
||
|
menu.Visible -= 1
|
||
|
elif entry.DesktopEntry.getTryExec() and not __try_exec(entry.DesktopEntry.getTryExec()):
|
||
|
entry.Show = "NoExec"
|
||
|
menu.Visible -= 1
|
||
|
elif xdg.Config.windowmanager:
|
||
|
if ( entry.DesktopEntry.getOnlyShowIn() != [] and xdg.Config.windowmanager not in entry.DesktopEntry.getOnlyShowIn() ) \
|
||
|
or xdg.Config.windowmanager in entry.DesktopEntry.getNotShowIn():
|
||
|
entry.Show = "NotShowIn"
|
||
|
menu.Visible -= 1
|
||
|
elif isinstance(entry,Separator):
|
||
|
menu.Visible -= 1
|
||
|
|
||
|
# remove separators at the beginning and at the end
|
||
|
if len(menu.Entries) > 0:
|
||
|
if isinstance(menu.Entries[0], Separator):
|
||
|
menu.Entries[0].Show = False
|
||
|
if len(menu.Entries) > 1:
|
||
|
if isinstance(menu.Entries[-1], Separator):
|
||
|
menu.Entries[-1].Show = False
|
||
|
|
||
|
# show_empty tag
|
||
|
for entry in menu.Entries[:]:
|
||
|
if isinstance(entry, Menu) and entry.Layout.show_empty == "false" and entry.Visible == 0:
|
||
|
entry.Show = "Empty"
|
||
|
menu.Visible -= 1
|
||
|
if entry.NotInXml == True:
|
||
|
menu.Entries.remove(entry)
|
||
|
|
||
|
def __try_exec(executable):
|
||
|
paths = os.environ['PATH'].split(os.pathsep)
|
||
|
if not os.path.isfile(executable):
|
||
|
for p in paths:
|
||
|
f = os.path.join(p, executable)
|
||
|
if os.path.isfile(f):
|
||
|
if os.access(f, os.X_OK):
|
||
|
return True
|
||
|
else:
|
||
|
if os.access(executable, os.X_OK):
|
||
|
return True
|
||
|
return False
|
||
|
|
||
|
# inline tags
|
||
|
def __parse_inline(submenu, menu):
|
||
|
if submenu.Layout.inline == "true":
|
||
|
if len(submenu.Entries) == 1 and submenu.Layout.inline_alias == "true":
|
||
|
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)
|
||
|
menu.Entries.append(menuentry)
|
||
|
elif len(submenu.Entries) <= submenu.Layout.inline_limit or submenu.Layout.inline_limit == 0:
|
||
|
if submenu.Layout.inline_header == "true":
|
||
|
header = Header(submenu.getName(), submenu.getGenericName(), submenu.getComment())
|
||
|
menu.Entries.append(header)
|
||
|
for entry in submenu.Entries:
|
||
|
menu.Entries.append(entry)
|
||
|
else:
|
||
|
menu.Entries.append(submenu)
|
||
|
else:
|
||
|
menu.Entries.append(submenu)
|
||
|
|
||
|
class MenuEntryCache:
|
||
|
"Class to cache Desktop Entries"
|
||
|
def __init__(self):
|
||
|
self.cacheEntries = {}
|
||
|
self.cacheEntries['legacy'] = []
|
||
|
self.cache = {}
|
||
|
|
||
|
def addMenuEntries(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 os.path.splitext(item)[1] == ".desktop":
|
||
|
try:
|
||
|
menuentry = MenuEntry(os.path.join(subdir,item), dir, prefix)
|
||
|
except ParsingError:
|
||
|
continue
|
||
|
|
||
|
self.cacheEntries[dir].append(menuentry)
|
||
|
if legacy == True:
|
||
|
self.cacheEntries['legacy'].append(menuentry)
|
||
|
elif os.path.isdir(os.path.join(dir,subdir,item)) and legacy == False:
|
||
|
self.__addFiles(dir, os.path.join(subdir,item), prefix, legacy)
|
||
|
|
||
|
def getMenuEntries(self, dirs, legacy=True):
|
||
|
list = []
|
||
|
ids = []
|
||
|
# handle legacy items
|
||
|
appdirs = dirs[:]
|
||
|
if legacy == True:
|
||
|
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.append(menuentry.DesktopFileID)
|
||
|
list.append(menuentry)
|
||
|
elif menuentry.getType() == "System":
|
||
|
# FIXME: This is only 99% correct, but still...
|
||
|
i = list.index(menuentry)
|
||
|
e = list[i]
|
||
|
if e.getType() == "User":
|
||
|
e.Original = menuentry
|
||
|
except UnicodeDecodeError:
|
||
|
continue
|
||
|
self.cache[key] = list
|
||
|
return list
|