# -*- coding: utf-8 -*-
# Mozo Menu Editor - Simple fd.o Compliant Menu Editor
# Copyright (C) 2006 Travis Watkins, Heinrich Wendel
# Copyright (C) 2012-2021 MATE Developers
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import codecs
import os
import xml.dom.minidom
import xml.parsers.expat
import locale
import gi
gi.require_version('MateMenu', '2.0')
from gi.repository import MateMenu, GLib
from Mozo import util
class Menu:
tree = None
visible_tree = None
path = None
dom = None
class MenuEditor(object):
def __init__(self, undo_button, redo_button):
self.undo_button = undo_button
self.redo_button = redo_button
self.locale = locale.getdefaultlocale()[0]
self.__undo = []
self.__redo = []
self.applications = Menu()
self.applications.tree = MateMenu.Tree.new('mate-applications.menu', MateMenu.TreeFlags.SHOW_EMPTY|MateMenu.TreeFlags.INCLUDE_EXCLUDED|MateMenu.TreeFlags.INCLUDE_NODISPLAY|MateMenu.TreeFlags.SHOW_ALL_SEPARATORS|MateMenu.TreeFlags.SORT_DISPLAY_NAME)
self.applications.visible_tree = MateMenu.Tree.new('mate-applications.menu', MateMenu.TreeFlags.SORT_DISPLAY_NAME)
self.applications.tree.sort_key = MateMenu.TreeFlags.SORT_DISPLAY_NAME
self.applications.visible_tree.sort_key = MateMenu.TreeFlags.SORT_DISPLAY_NAME
self.applications.tree.connect('changed', self.menuChanged)
self.settings = Menu()
self.settings.tree = MateMenu.Tree.new('mate-settings.menu', MateMenu.TreeFlags.SHOW_EMPTY|MateMenu.TreeFlags.INCLUDE_EXCLUDED|MateMenu.TreeFlags.INCLUDE_NODISPLAY|MateMenu.TreeFlags.SHOW_ALL_SEPARATORS|MateMenu.TreeFlags.SORT_DISPLAY_NAME)
self.settings.visible_tree = MateMenu.Tree.new('mate-settings.menu', MateMenu.TreeFlags.SORT_DISPLAY_NAME)
self.settings.tree.sort_key = MateMenu.TreeFlags.SORT_DISPLAY_NAME
self.settings.visible_tree.sort_key = MateMenu.TreeFlags.SORT_DISPLAY_NAME
self.settings.tree.connect('changed', self.menuChanged)
self.load()
self.__loadMenus()
self.update_undo_redo_button_state()
def __loadMenus(self):
self.applications.path = os.path.join(util.getUserMenuPath(), self.applications.tree.props.menu_basename)
try:
self.applications.dom = xml.dom.minidom.parse(self.applications.path)
except (IOError, xml.parsers.expat.ExpatError):
self.applications.dom = xml.dom.minidom.parseString(util.getUserMenuXml(self.applications.tree))
util.removeWhitespaceNodes(self.applications.dom)
self.settings.path = os.path.join(util.getUserMenuPath(), self.settings.tree.props.menu_basename)
try:
self.settings.dom = xml.dom.minidom.parse(self.settings.path)
except (IOError, xml.parsers.expat.ExpatError):
self.settings.dom = xml.dom.minidom.parseString(util.getUserMenuXml(self.settings.tree))
util.removeWhitespaceNodes(self.settings.dom)
self.save(True)
def load(self):
if not self.applications.tree.load_sync():
raise ValueError("can not load menu tree %r" % (self.applications.tree.props.menu_basename,))
if not self.settings.tree.load_sync():
raise ValueError("can not load menu tree %r" % (self.settings.tree.props.menu_basename,))
if not self.applications.visible_tree.load_sync():
raise ValueError("can not load menu tree %r" % (self.applications.visible_tree.props.menu_basename,))
if not self.settings.visible_tree.load_sync():
raise ValueError("can not load menu tree %r" % (self.settings.visible_tree.props.menu_basename,))
def menuChanged(self, *a):
self.load()
def save(self, from_loading=False):
for menu in ('applications', 'settings'):
with codecs.open(getattr(self, menu).path, 'w', 'utf-8') as f:
f.write(getattr(self, menu).dom.toprettyxml())
if not from_loading:
self.load()
self.__loadMenus()
def quit(self):
for file_name in os.listdir(util.getUserItemPath()):
if file_name[-6:-2] in ('redo', 'undo'):
file_path = os.path.join(util.getUserItemPath(), file_name)
os.unlink(file_path)
for file_name in os.listdir(util.getUserDirectoryPath()):
if file_name[-6:-2] in ('redo', 'undo'):
file_path = os.path.join(util.getUserDirectoryPath(), file_name)
os.unlink(file_path)
for file_name in os.listdir(util.getUserMenuPath()):
if file_name[-6:-2] in ('redo', 'undo'):
file_path = os.path.join(util.getUserMenuPath(), file_name)
os.unlink(file_path)
def revert(self):
for name in ('applications', 'settings'):
menu = getattr(self, name)
self.revertTree(menu.tree.get_root_directory())
path = os.path.join(util.getUserMenuPath(), menu.tree.props.menu_basename)
try:
os.unlink(path)
except OSError:
pass
#reload DOM for each menu
try:
menu.dom = xml.dom.minidom.parse(menu.path)
except (IOError, xml.parsers.expat.ExpatError):
menu.dom = xml.dom.minidom.parseString(util.getUserMenuXml(menu.tree))
util.removeWhitespaceNodes(menu.dom)
#reset undo/redo, no way to recover from this
self.__undo, self.__redo = [], []
self.update_undo_redo_button_state()
self.save()
def revertTree(self, menu):
item_iter = menu.iter()
item_type = item_iter.next()
while item_type != MateMenu.TreeItemType.INVALID:
if item_type == MateMenu.TreeItemType.DIRECTORY:
item = item_iter.get_directory()
self.revertTree(item)
elif item_type == MateMenu.TreeItemType.ENTRY:
item = item_iter.get_entry()
self.revertItem(item)
item_type = item_iter.next()
self.revertMenu(menu)
def revertItem(self, item):
if not self.canRevert(item):
return
self.__addUndo([item,])
try:
os.remove(item.get_desktop_file_path())
except OSError:
pass
self.save()
def revertMenu(self, menu):
if not self.canRevert(menu):
return
#wtf happened here? oh well, just bail
if not menu.get_desktop_file_path():
return
self.__addUndo([menu,])
file_id = os.path.split(menu.get_desktop_file_path())[1]
path = os.path.join(util.getUserDirectoryPath(), file_id)
try:
os.remove(path)
except OSError:
pass
self.save()
def update_undo_redo_button_state(self):
self.redo_button.set_sensitive(len(self.__redo) > 0)
self.undo_button.set_sensitive(len(self.__undo) > 0)
def undo(self):
if len(self.__undo) == 0:
return
files = self.__undo.pop()
redo = []
for undo_path in files[::-1]:
new_path = undo_path.rsplit('.', 1)[0]
if not os.path.exists(undo_path):
continue
redo_path = util.getUniqueRedoFile(new_path)
# create redo file
try:
with codecs.open(new_path, 'r', 'utf-8') as f_new:
with codecs.open(redo_path, 'w', 'utf-8') as f_redo:
f_redo.write(f_new.read())
redo.append(redo_path)
except FileNotFoundError:
pass
# restore undo file
try:
with codecs.open(undo_path, 'r', 'utf-8') as f_undo:
with codecs.open(new_path, 'w', 'utf-8') as f_new:
f_new.write(f_undo.read())
os.unlink(undo_path)
except FileNotFoundError:
pass
# reload DOM to make changes stick
for name in ('applications', 'settings'):
menu = getattr(self, name)
try:
menu.dom = xml.dom.minidom.parse(menu.path)
except (IOError, xml.parsers.expat.ExpatError):
menu.dom = xml.dom.minidom.parseString(util.getUserMenuXml(menu.tree))
util.removeWhitespaceNodes(menu.dom)
if redo:
self.__redo.append(redo)
self.update_undo_redo_button_state()
def redo(self):
if len(self.__redo) == 0:
return
files = self.__redo.pop()
undo = []
for redo_path in files[::-1]:
new_path = redo_path.rsplit('.', 1)[0]
if not os.path.exists(redo_path):
continue
undo_path = util.getUniqueUndoFile(new_path)
# create undo file
try:
with codecs.open(new_path, 'r', 'utf-8') as f_new:
with codecs.open(undo_path, 'w', 'utf-8') as f_undo:
f_undo.write(f_new.read())
undo.append(undo_path)
except FileNotFoundError:
pass
# restore redo file
try:
with codecs.open(redo_path, 'r', 'utf-8') as f_redo:
with codecs.open(new_path, 'w', 'utf-8') as f_new:
f_new.write(f_redo.read())
os.unlink(redo_path)
except FileNotFoundError:
pass
#reload DOM to make changes stick
for name in ('applications', 'settings'):
menu = getattr(self, name)
try:
menu.dom = xml.dom.minidom.parse(menu.path)
except (IOError, xml.parsers.expat.ExpatError):
menu.dom = xml.dom.minidom.parseString(util.getUserMenuXml(menu.tree))
util.removeWhitespaceNodes(menu.dom)
if undo:
self.__undo.append(undo)
self.update_undo_redo_button_state()
def getMenus(self, parent=None):
if parent is None:
yield self.applications.tree.get_root_directory()
yield self.settings.tree.get_root_directory()
else:
item_iter = parent.iter()
item_type = item_iter.next()
while item_type != MateMenu.TreeItemType.INVALID:
if item_type == MateMenu.TreeItemType.DIRECTORY:
item = item_iter.get_directory()
if item.get_menu_id() != 'Collection':
yield (item, self.__isVisible(item))
item_type = item_iter.next()
def getContents(self, item):
class CollectionDirectoryException(Exception): pass
contents = []
item_iter = item.iter()
item_type = item_iter.next()
while item_type != MateMenu.TreeItemType.INVALID:
item = None
try:
if item_type == MateMenu.TreeItemType.DIRECTORY:
item = item_iter.get_directory()
if item.get_menu_id() == 'Collection': raise CollectionDirectoryException()
elif item_type == MateMenu.TreeItemType.ENTRY:
item = item_iter.get_entry()
elif item_type == MateMenu.TreeItemType.HEADER:
item = item_iter.get_header()
elif item_type == MateMenu.TreeItemType.ALIAS:
item = item_iter.get_alias()
elif item_type == MateMenu.TreeItemType.SEPARATOR:
item = item_iter.get_separator()
if item:
contents.append(item)
except CollectionDirectoryException:
pass
item_type = item_iter.next()
return contents
def getItems(self, menu):
for item in self.getContents(menu):
if isinstance(item, MateMenu.TreeSeparator):
yield(item, True)
else:
if isinstance(item, MateMenu.TreeEntry) and item.get_desktop_file_id()[-19:] == '-usercustom.desktop':
continue
yield (item, self.__isVisible(item))
def canRevert(self, item):
if isinstance(item, MateMenu.TreeEntry):
if util.getItemPath(item.get_desktop_file_id()) is not None:
path = util.getUserItemPath()
if os.path.isfile(os.path.join(path, item.get_desktop_file_id())):
return True
elif isinstance(item, MateMenu.TreeDirectory):
if item.get_desktop_file_path():
file_id = os.path.split(item.get_desktop_file_path())[1]
else:
file_id = item.get_menu_id() + '.directory'
if util.getDirectoryPath(file_id) is not None:
path = util.getUserDirectoryPath()
if os.path.isfile(os.path.join(path, file_id)):
return True
return False
def setVisible(self, item, visible):
dom = self.__getMenu(item).dom
if isinstance(item, MateMenu.TreeEntry):
self.__addUndo([self.__getMenu(item), item])
menu_xml = self.__getXmlMenu(self.__getPath(item.get_parent()), dom.documentElement, dom)
if visible:
self.__addXmlFilename(menu_xml, dom, item.get_desktop_file_id(), 'Include')
self.__writeItem(item, NoDisplay=False)
else:
self.__addXmlFilename(menu_xml, dom, item.get_desktop_file_id(), 'Exclude')
self.__addXmlTextElement(menu_xml, 'AppDir', util.getUserItemPath(), dom)
elif isinstance(item, MateMenu.TreeDirectory):
self.__addUndo([self.__getMenu(item), item])
item_iter = item.iter()
first_child_type = item_iter.next()
#don't mess with it if it's empty
if first_child_type == MateMenu.TreeItemType.INVALID:
return
menu_xml = self.__getXmlMenu(self.__getPath(item), dom.documentElement, dom)
for node in self.__getXmlNodesByName(['Deleted', 'NotDeleted'], menu_xml):
node.parentNode.removeChild(node)
self.__writeMenu(item, NoDisplay=not visible)
self.__addXmlTextElement(menu_xml, 'DirectoryDir', util.getUserDirectoryPath(), dom)
self.save()
def createItem(self, parent, before, after, **kwargs):
file_id = self.__writeItem(None, **kwargs)
self.insertExternalItem(file_id, parent.get_menu_id(), before, after)
def insertExternalItem(self, file_id, parent_id, before=None, after=None):
parent = self.__findMenu(parent_id)
dom = self.__getMenu(parent).dom
self.__addItem(parent, file_id, dom)
self.__positionItem(parent, ('Item', file_id), before, after)
self.__addUndo([self.__getMenu(parent), ('Item', file_id)])
self.save()
def insertExternalMenu(self, file_id, parent_id, before=None, after=None):
menu_id = file_id.rsplit('.', 1)[0]
parent = self.__findMenu(parent_id)
dom = self.__getMenu(parent).dom
self.__addXmlDefaultLayout(self.__getXmlMenu(self.__getPath(parent), dom.documentElement, dom) , dom)
menu_xml = self.__getXmlMenu(self.__getPath(parent) + [menu_id], dom.documentElement, dom)
self.__addXmlTextElement(menu_xml, 'Directory', file_id, dom)
self.__positionItem(parent, ('Menu', menu_id), before, after)
self.__addUndo([self.__getMenu(parent), ('Menu', file_id)])
self.save()
def createSeparator(self, parent, before=None, after=None):
self.__positionItem(parent, ('Separator',), before, after)
self.__addUndo([self.__getMenu(parent), ('Separator',)])
self.save()
def editItem(self, item, icon, name, comment, command, use_term, parent=None, final=True):
#if nothing changed don't make a user copy
app_info = item.get_app_info()
if icon == app_info.get_icon() and name == app_info.get_display_name() and comment == item.get_comment() and command == item.get_exec() and use_term == item.get_launch_in_terminal():
return
#hack, item.get_parent() seems to fail a lot
if not parent:
parent = item.get_parent()
if final:
self.__addUndo([self.__getMenu(parent), item])
self.__writeItem(item, Icon=icon, Name=name, Comment=comment, Exec=command, Terminal=use_term)
if final:
dom = self.__getMenu(parent).dom
menu_xml = self.__getXmlMenu(self.__getPath(parent), dom.documentElement, dom)
self.__addXmlTextElement(menu_xml, 'AppDir', util.getUserItemPath(), dom)
self.save()
def editMenu(self, menu, icon, name, comment, final=True):
#if nothing changed don't make a user copy
if icon == menu.get_icon() and name == menu.get_name() and comment == menu.get_comment():
return
#we don't use this, we just need to make sure the