bazarr/libs/xdg/MenuEditor.py
2018-10-31 17:08:29 +01:00

511 lines
18 KiB
Python

""" CLass to edit XDG Menus """
from xdg.Menu import *
from xdg.BaseDirectory import *
from xdg.Exceptions import *
from xdg.DesktopEntry import *
from xdg.Config import *
import xml.dom.minidom
import os
import re
# XML-Cleanups: Move / Exclude
# FIXME: proper reverte/delete
# FIXME: pass AppDirs/DirectoryDirs around in the edit/move functions
# FIXME: catch Exceptions
# FIXME: copy functions
# FIXME: More Layout stuff
# FIXME: unod/redo function / remove menu...
# FIXME: Advanced MenuEditing Stuff: LegacyDir/MergeFile
# Complex Rules/Deleted/OnlyAllocated/AppDirs/DirectoryDirs
class MenuEditor:
def __init__(self, menu=None, filename=None, root=False):
self.menu = None
self.filename = None
self.doc = None
self.parse(menu, filename, root)
# fix for creating two menus with the same name on the fly
self.filenames = []
def parse(self, menu=None, filename=None, root=False):
if root == True:
setRootMode(True)
if isinstance(menu, Menu):
self.menu = menu
elif menu:
self.menu = parse(menu)
else:
self.menu = parse()
if root == True:
self.filename = self.menu.Filename
elif filename:
self.filename = filename
else:
self.filename = os.path.join(xdg_config_dirs[0], "menus", os.path.split(self.menu.Filename)[1])
try:
self.doc = xml.dom.minidom.parse(self.filename)
except IOError:
self.doc = xml.dom.minidom.parseString('<!DOCTYPE Menu PUBLIC "-//freedesktop//DTD Menu 1.0//EN" "http://standards.freedesktop.org/menu-spec/menu-1.0.dtd"><Menu><Name>Applications</Name><MergeFile type="parent">'+self.menu.Filename+'</MergeFile></Menu>')
except xml.parsers.expat.ExpatError:
raise ParsingError('Not a valid .menu file', self.filename)
self.__remove_whilespace_nodes(self.doc)
def save(self):
self.__saveEntries(self.menu)
self.__saveMenu()
def createMenuEntry(self, parent, name, command=None, genericname=None, comment=None, icon=None, terminal=None, after=None, before=None):
menuentry = MenuEntry(self.__getFileName(name, ".desktop"))
menuentry = self.editMenuEntry(menuentry, name, genericname, comment, command, icon, terminal)
self.__addEntry(parent, menuentry, after, before)
sort(self.menu)
return menuentry
def createMenu(self, parent, name, genericname=None, comment=None, icon=None, after=None, before=None):
menu = Menu()
menu.Parent = parent
menu.Depth = parent.Depth + 1
menu.Layout = parent.DefaultLayout
menu.DefaultLayout = parent.DefaultLayout
menu = self.editMenu(menu, name, genericname, comment, icon)
self.__addEntry(parent, menu, after, before)
sort(self.menu)
return menu
def createSeparator(self, parent, after=None, before=None):
separator = Separator(parent)
self.__addEntry(parent, separator, after, before)
sort(self.menu)
return separator
def moveMenuEntry(self, menuentry, oldparent, newparent, after=None, before=None):
self.__deleteEntry(oldparent, menuentry, after, before)
self.__addEntry(newparent, menuentry, after, before)
sort(self.menu)
return menuentry
def moveMenu(self, menu, oldparent, newparent, after=None, before=None):
self.__deleteEntry(oldparent, menu, after, before)
self.__addEntry(newparent, menu, after, before)
root_menu = self.__getXmlMenu(self.menu.Name)
if oldparent.getPath(True) != newparent.getPath(True):
self.__addXmlMove(root_menu, os.path.join(oldparent.getPath(True), menu.Name), os.path.join(newparent.getPath(True), menu.Name))
sort(self.menu)
return menu
def moveSeparator(self, separator, parent, after=None, before=None):
self.__deleteEntry(parent, separator, after, before)
self.__addEntry(parent, separator, after, before)
sort(self.menu)
return separator
def copyMenuEntry(self, menuentry, oldparent, newparent, after=None, before=None):
self.__addEntry(newparent, menuentry, after, before)
sort(self.menu)
return menuentry
def editMenuEntry(self, menuentry, name=None, genericname=None, comment=None, command=None, icon=None, terminal=None, nodisplay=None, hidden=None):
deskentry = menuentry.DesktopEntry
if name:
if not deskentry.hasKey("Name"):
deskentry.set("Name", name)
deskentry.set("Name", name, locale = True)
if comment:
if not deskentry.hasKey("Comment"):
deskentry.set("Comment", comment)
deskentry.set("Comment", comment, locale = True)
if genericname:
if not deskentry.hasKey("GnericNe"):
deskentry.set("GenericName", genericname)
deskentry.set("GenericName", genericname, locale = True)
if command:
deskentry.set("Exec", command)
if icon:
deskentry.set("Icon", icon)
if terminal == True:
deskentry.set("Terminal", "true")
elif terminal == False:
deskentry.set("Terminal", "false")
if nodisplay == True:
deskentry.set("NoDisplay", "true")
elif nodisplay == False:
deskentry.set("NoDisplay", "false")
if hidden == True:
deskentry.set("Hidden", "true")
elif hidden == False:
deskentry.set("Hidden", "false")
menuentry.updateAttributes()
if len(menuentry.Parents) > 0:
sort(self.menu)
return menuentry
def editMenu(self, menu, name=None, genericname=None, comment=None, icon=None, nodisplay=None, hidden=None):
# Hack for legacy dirs
if isinstance(menu.Directory, MenuEntry) and menu.Directory.Filename == ".directory":
xml_menu = self.__getXmlMenu(menu.getPath(True, True))
self.__addXmlTextElement(xml_menu, 'Directory', menu.Name + ".directory")
menu.Directory.setAttributes(menu.Name + ".directory")
# Hack for New Entries
elif not isinstance(menu.Directory, MenuEntry):
if not name:
name = menu.Name
filename = self.__getFileName(name, ".directory").replace("/", "")
if not menu.Name:
menu.Name = filename.replace(".directory", "")
xml_menu = self.__getXmlMenu(menu.getPath(True, True))
self.__addXmlTextElement(xml_menu, 'Directory', filename)
menu.Directory = MenuEntry(filename)
deskentry = menu.Directory.DesktopEntry
if name:
if not deskentry.hasKey("Name"):
deskentry.set("Name", name)
deskentry.set("Name", name, locale = True)
if genericname:
if not deskentry.hasKey("GenericName"):
deskentry.set("GenericName", genericname)
deskentry.set("GenericName", genericname, locale = True)
if comment:
if not deskentry.hasKey("Comment"):
deskentry.set("Comment", comment)
deskentry.set("Comment", comment, locale = True)
if icon:
deskentry.set("Icon", icon)
if nodisplay == True:
deskentry.set("NoDisplay", "true")
elif nodisplay == False:
deskentry.set("NoDisplay", "false")
if hidden == True:
deskentry.set("Hidden", "true")
elif hidden == False:
deskentry.set("Hidden", "false")
menu.Directory.updateAttributes()
if isinstance(menu.Parent, Menu):
sort(self.menu)
return menu
def hideMenuEntry(self, menuentry):
self.editMenuEntry(menuentry, nodisplay = True)
def unhideMenuEntry(self, menuentry):
self.editMenuEntry(menuentry, nodisplay = False, hidden = False)
def hideMenu(self, menu):
self.editMenu(menu, nodisplay = True)
def unhideMenu(self, menu):
self.editMenu(menu, nodisplay = False, hidden = False)
xml_menu = self.__getXmlMenu(menu.getPath(True,True), False)
for node in self.__getXmlNodesByName(["Deleted", "NotDeleted"], xml_menu):
node.parentNode.removeChild(node)
def deleteMenuEntry(self, menuentry):
if self.getAction(menuentry) == "delete":
self.__deleteFile(menuentry.DesktopEntry.filename)
for parent in menuentry.Parents:
self.__deleteEntry(parent, menuentry)
sort(self.menu)
return menuentry
def revertMenuEntry(self, menuentry):
if self.getAction(menuentry) == "revert":
self.__deleteFile(menuentry.DesktopEntry.filename)
menuentry.Original.Parents = []
for parent in menuentry.Parents:
index = parent.Entries.index(menuentry)
parent.Entries[index] = menuentry.Original
index = parent.MenuEntries.index(menuentry)
parent.MenuEntries[index] = menuentry.Original
menuentry.Original.Parents.append(parent)
sort(self.menu)
return menuentry
def deleteMenu(self, menu):
if self.getAction(menu) == "delete":
self.__deleteFile(menu.Directory.DesktopEntry.filename)
self.__deleteEntry(menu.Parent, menu)
xml_menu = self.__getXmlMenu(menu.getPath(True, True))
xml_menu.parentNode.removeChild(xml_menu)
sort(self.menu)
return menu
def revertMenu(self, menu):
if self.getAction(menu) == "revert":
self.__deleteFile(menu.Directory.DesktopEntry.filename)
menu.Directory = menu.Directory.Original
sort(self.menu)
return menu
def deleteSeparator(self, separator):
self.__deleteEntry(separator.Parent, separator, after=True)
sort(self.menu)
return separator
""" Private Stuff """
def getAction(self, entry):
if isinstance(entry, Menu):
if not isinstance(entry.Directory, MenuEntry):
return "none"
elif entry.Directory.getType() == "Both":
return "revert"
elif entry.Directory.getType() == "User" \
and (len(entry.Submenus) + len(entry.MenuEntries)) == 0:
return "delete"
elif isinstance(entry, MenuEntry):
if entry.getType() == "Both":
return "revert"
elif entry.getType() == "User":
return "delete"
else:
return "none"
return "none"
def __saveEntries(self, menu):
if not menu:
menu = self.menu
if isinstance(menu.Directory, MenuEntry):
menu.Directory.save()
for entry in menu.getEntries(hidden=True):
if isinstance(entry, MenuEntry):
entry.save()
elif isinstance(entry, Menu):
self.__saveEntries(entry)
def __saveMenu(self):
if not os.path.isdir(os.path.dirname(self.filename)):
os.makedirs(os.path.dirname(self.filename))
fd = open(self.filename, 'w')
fd.write(re.sub("\n[\s]*([^\n<]*)\n[\s]*</", "\\1</", self.doc.toprettyxml().replace('<?xml version="1.0" ?>\n', '')))
fd.close()
def __getFileName(self, name, extension):
postfix = 0
while 1:
if postfix == 0:
filename = name + extension
else:
filename = name + "-" + str(postfix) + extension
if extension == ".desktop":
dir = "applications"
elif extension == ".directory":
dir = "desktop-directories"
if not filename in self.filenames and not \
os.path.isfile(os.path.join(xdg_data_dirs[0], dir, filename)):
self.filenames.append(filename)
break
else:
postfix += 1
return filename
def __getXmlMenu(self, path, create=True, element=None):
if not element:
element = self.doc
if "/" in path:
(name, path) = path.split("/", 1)
else:
name = path
path = ""
found = None
for node in self.__getXmlNodesByName("Menu", element):
for child in self.__getXmlNodesByName("Name", node):
if child.childNodes[0].nodeValue == name:
if path:
found = self.__getXmlMenu(path, create, node)
else:
found = node
break
if found:
break
if not found and create == True:
node = self.__addXmlMenuElement(element, name)
if path:
found = self.__getXmlMenu(path, create, node)
else:
found = node
return found
def __addXmlMenuElement(self, element, name):
node = self.doc.createElement('Menu')
self.__addXmlTextElement(node, 'Name', name)
return element.appendChild(node)
def __addXmlTextElement(self, element, name, text):
node = self.doc.createElement(name)
text = self.doc.createTextNode(text)
node.appendChild(text)
return element.appendChild(node)
def __addXmlFilename(self, element, filename, type = "Include"):
# remove old filenames
for node in self.__getXmlNodesByName(["Include", "Exclude"], element):
if node.childNodes[0].nodeName == "Filename" and node.childNodes[0].childNodes[0].nodeValue == filename:
element.removeChild(node)
# add new filename
node = self.doc.createElement(type)
node.appendChild(self.__addXmlTextElement(node, 'Filename', filename))
return element.appendChild(node)
def __addXmlMove(self, element, old, new):
node = self.doc.createElement("Move")
node.appendChild(self.__addXmlTextElement(node, 'Old', old))
node.appendChild(self.__addXmlTextElement(node, 'New', new))
return element.appendChild(node)
def __addXmlLayout(self, element, layout):
# remove old layout
for node in self.__getXmlNodesByName("Layout", element):
element.removeChild(node)
# add new layout
node = self.doc.createElement("Layout")
for order in layout.order:
if order[0] == "Separator":
child = self.doc.createElement("Separator")
node.appendChild(child)
elif order[0] == "Filename":
child = self.__addXmlTextElement(node, "Filename", order[1])
elif order[0] == "Menuname":
child = self.__addXmlTextElement(node, "Menuname", order[1])
elif order[0] == "Merge":
child = self.doc.createElement("Merge")
child.setAttribute("type", order[1])
node.appendChild(child)
return element.appendChild(node)
def __getXmlNodesByName(self, name, element):
for child in element.childNodes:
if child.nodeType == xml.dom.Node.ELEMENT_NODE and child.nodeName in name:
yield child
def __addLayout(self, parent):
layout = Layout()
layout.order = []
layout.show_empty = parent.Layout.show_empty
layout.inline = parent.Layout.inline
layout.inline_header = parent.Layout.inline_header
layout.inline_alias = parent.Layout.inline_alias
layout.inline_limit = parent.Layout.inline_limit
layout.order.append(["Merge", "menus"])
for entry in parent.Entries:
if isinstance(entry, Menu):
layout.parseMenuname(entry.Name)
elif isinstance(entry, MenuEntry):
layout.parseFilename(entry.DesktopFileID)
elif isinstance(entry, Separator):
layout.parseSeparator()
layout.order.append(["Merge", "files"])
parent.Layout = layout
return layout
def __addEntry(self, parent, entry, after=None, before=None):
if after or before:
if after:
index = parent.Entries.index(after) + 1
elif before:
index = parent.Entries.index(before)
parent.Entries.insert(index, entry)
else:
parent.Entries.append(entry)
xml_parent = self.__getXmlMenu(parent.getPath(True, True))
if isinstance(entry, MenuEntry):
parent.MenuEntries.append(entry)
entry.Parents.append(parent)
self.__addXmlFilename(xml_parent, entry.DesktopFileID, "Include")
elif isinstance(entry, Menu):
parent.addSubmenu(entry)
if after or before:
self.__addLayout(parent)
self.__addXmlLayout(xml_parent, parent.Layout)
def __deleteEntry(self, parent, entry, after=None, before=None):
parent.Entries.remove(entry)
xml_parent = self.__getXmlMenu(parent.getPath(True, True))
if isinstance(entry, MenuEntry):
entry.Parents.remove(parent)
parent.MenuEntries.remove(entry)
self.__addXmlFilename(xml_parent, entry.DesktopFileID, "Exclude")
elif isinstance(entry, Menu):
parent.Submenus.remove(entry)
if after or before:
self.__addLayout(parent)
self.__addXmlLayout(xml_parent, parent.Layout)
def __deleteFile(self, filename):
try:
os.remove(filename)
except OSError:
pass
try:
self.filenames.remove(filename)
except ValueError:
pass
def __remove_whilespace_nodes(self, node):
remove_list = []
for child in node.childNodes:
if child.nodeType == xml.dom.minidom.Node.TEXT_NODE:
child.data = child.data.strip()
if not child.data.strip():
remove_list.append(child)
elif child.hasChildNodes():
self.__remove_whilespace_nodes(child)
for node in remove_list:
node.parentNode.removeChild(node)