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.
512 lines
18 KiB
512 lines
18 KiB
6 years ago
|
""" 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)
|