1 # -*- coding: utf-8 -*-
2 # Alacarte Menu Editor - Simple fd.o Compliant Menu Editor
3 # Copyright (C) 2006 Travis Watkins, Heinrich Wendel
5 # This library is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU Library General Public
7 # License as published by the Free Software Foundation; either
8 # version 2 of the License, or (at your option) any later version.
10 # This library is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 # Library General Public License for more details.
15 # You should have received a copy of the GNU Library General Public
16 # License along with this library; if not, write to the Free Software
17 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
21 import xml
.dom
.minidom
22 import xml
.parsers
.expat
23 from gi
.repository
import GMenu
, GLib
24 from Alacarte
import util
26 class MenuEditor(object):
27 def __init__(self
, name
=os
.environ
.get('XDG_MENU_PREFIX', '') + 'applications.menu'):
30 self
.tree
= GMenu
.Tree
.new(name
, GMenu
.TreeFlags
.SHOW_EMPTY|GMenu
.TreeFlags
.INCLUDE_EXCLUDED|GMenu
.TreeFlags
.INCLUDE_NODISPLAY|GMenu
.TreeFlags
.SHOW_ALL_SEPARATORS|GMenu
.TreeFlags
.SORT_DISPLAY_NAME
)
31 self
.tree
.connect('changed', self
.menuChanged
)
34 self
.path
= os
.path
.join(util
.getUserMenuPath(), self
.tree
.props
.menu_basename
)
39 self
.dom
= xml
.dom
.minidom
.parse(self
.path
)
40 except (IOError, xml
.parsers
.expat
.ExpatError
), e
:
41 self
.dom
= xml
.dom
.minidom
.parseString(util
.getUserMenuXml(self
.tree
))
42 util
.removeWhitespaceNodes(self
.dom
)
45 if not self
.tree
.load_sync():
46 raise ValueError("can not load menu tree %r" % (self
.name
,))
48 def menuChanged(self
, *a
):
52 with codecs
.open(self
.path
, 'w', 'utf8') as f
:
53 f
.write(self
.dom
.toprettyxml())
55 def restoreToSystem(self
):
56 self
.restoreTree(self
.tree
.get_root_directory())
57 path
= os
.path
.join(util
.getUserMenuPath(), os
.path
.basename(self
.tree
.get_canonical_menu_path()))
66 def restoreTree(self
, menu
):
67 item_iter
= menu
.iter()
68 item_type
= item_iter
.next()
69 while item_type
!= GMenu
.TreeItemType
.INVALID
:
70 if item_type
== GMenu
.TreeItemType
.DIRECTORY
:
71 item
= item_iter
.get_directory()
72 self
.restoreTree(item
)
73 elif item_type
== GMenu
.TreeItemType
.ENTRY
:
74 item
= item_iter
.get_entry()
75 self
.restoreItem(item
)
76 item_type
= item_iter
.next()
77 self
.restoreMenu(menu
)
79 def restoreItem(self
, item
):
80 if not self
.canRevert(item
):
83 os
.remove(item
.get_desktop_file_path())
88 def restoreMenu(self
, menu
):
89 if not self
.canRevert(menu
):
91 #wtf happened here? oh well, just bail
92 if not menu
.get_desktop_file_path():
94 file_id
= os
.path
.split(menu
.get_desktop_file_path())[1]
95 path
= os
.path
.join(util
.getUserDirectoryPath(), file_id
)
102 def getMenus(self
, parent
):
104 yield (self
.tree
.get_root_directory(), True)
107 item_iter
= parent
.iter()
108 item_type
= item_iter
.next()
109 while item_type
!= GMenu
.TreeItemType
.INVALID
:
110 if item_type
== GMenu
.TreeItemType
.DIRECTORY
:
111 item
= item_iter
.get_directory()
112 yield (item
, self
.isVisible(item
))
113 item_type
= item_iter
.next()
115 def getContents(self
, item
):
117 item_iter
= item
.iter()
118 item_type
= item_iter
.next()
120 while item_type
!= GMenu
.TreeItemType
.INVALID
:
122 if item_type
== GMenu
.TreeItemType
.DIRECTORY
:
123 item
= item_iter
.get_directory()
124 elif item_type
== GMenu
.TreeItemType
.ENTRY
:
125 item
= item_iter
.get_entry()
126 elif item_type
== GMenu
.TreeItemType
.HEADER
:
127 item
= item_iter
.get_header()
128 elif item_type
== GMenu
.TreeItemType
.ALIAS
:
129 item
= item_iter
.get_alias()
130 elif item_type
== GMenu
.TreeItemType
.SEPARATOR
:
131 item
= item_iter
.get_separator()
133 contents
.append(item
)
134 item_type
= item_iter
.next()
137 def getItems(self
, menu
):
138 item_iter
= menu
.iter()
139 item_type
= item_iter
.next()
140 while item_type
!= GMenu
.TreeItemType
.INVALID
:
142 if item_type
== GMenu
.TreeItemType
.ENTRY
:
143 item
= item_iter
.get_entry()
144 elif item_type
== GMenu
.TreeItemType
.DIRECTORY
:
145 item
= item_iter
.get_directory()
146 elif item_type
== GMenu
.TreeItemType
.HEADER
:
147 item
= item_iter
.get_header()
148 elif item_type
== GMenu
.TreeItemType
.ALIAS
:
149 item
= item_iter
.get_alias()
150 elif item_type
== GMenu
.TreeItemType
.SEPARATOR
:
151 item
= item_iter
.get_separator()
152 yield (item
, self
.isVisible(item
))
153 item_type
= item_iter
.next()
155 def canRevert(self
, item
):
156 if isinstance(item
, GMenu
.TreeEntry
):
157 if util
.getItemPath(item
.get_desktop_file_id()) is not None:
158 path
= util
.getUserItemPath()
159 if os
.path
.isfile(os
.path
.join(path
, item
.get_desktop_file_id())):
161 elif isinstance(item
, GMenu
.TreeDirectory
):
162 if item
.get_desktop_file_path():
163 file_id
= os
.path
.split(item
.get_desktop_file_path())[1]
165 file_id
= item
.get_menu_id() + '.directory'
166 if util
.getDirectoryPath(file_id
) is not None:
167 path
= util
.getUserDirectoryPath()
168 if os
.path
.isfile(os
.path
.join(path
, file_id
)):
172 def setVisible(self
, item
, visible
):
174 if isinstance(item
, GMenu
.TreeEntry
):
175 menu_xml
= self
.getXmlMenu(self
.getPath(item
.get_parent()), dom
.documentElement
, dom
)
177 self
.addXmlFilename(menu_xml
, dom
, item
.get_desktop_file_id(), 'Include')
178 self
.writeItem(item
, NoDisplay
=False)
180 self
.addXmlFilename(menu_xml
, dom
, item
.get_desktop_file_id(), 'Exclude')
181 self
.addXmlTextElement(menu_xml
, 'AppDir', util
.getUserItemPath(), dom
)
182 elif isinstance(item
, GMenu
.TreeDirectory
):
183 item_iter
= item
.iter()
184 first_child_type
= item_iter
.next()
185 #don't mess with it if it's empty
186 if first_child_type
== GMenu
.TreeItemType
.INVALID
:
188 menu_xml
= self
.getXmlMenu(self
.getPath(item
), dom
.documentElement
, dom
)
189 for node
in self
.getXmlNodesByName(['Deleted', 'NotDeleted'], menu_xml
):
190 node
.parentNode
.removeChild(node
)
191 self
.writeMenu(item
, NoDisplay
=not visible
)
192 self
.addXmlTextElement(menu_xml
, 'DirectoryDir', util
.getUserDirectoryPath(), dom
)
195 def createItem(self
, parent
, before
, after
, **kwargs
):
196 file_id
= self
.writeItem(None, **kwargs
)
197 self
.insertExternalItem(file_id
, parent
.get_menu_id(), before
, after
)
199 def insertExternalItem(self
, file_id
, parent_id
, before
=None, after
=None):
200 parent
= self
.findMenu(parent_id
)
202 self
.addItem(parent
, file_id
, dom
)
203 self
.positionItem(parent
, ('Item', file_id
), before
, after
)
206 def insertExternalMenu(self
, file_id
, parent_id
, before
=None, after
=None):
207 menu_id
= file_id
.rsplit('.', 1)[0]
208 parent
= self
.findMenu(parent_id
)
210 self
.addXmlDefaultLayout(self
.getXmlMenu(self
.getPath(parent
), dom
.documentElement
, dom
) , dom
)
211 menu_xml
= self
.getXmlMenu(self
.getPath(parent
) + [menu_id
], dom
.documentElement
, dom
)
212 self
.addXmlTextElement(menu_xml
, 'Directory', file_id
, dom
)
213 self
.positionItem(parent
, ('Menu', menu_id
), before
, after
)
216 def createSeparator(self
, parent
, before
=None, after
=None):
217 self
.positionItem(parent
, ('Separator',), before
, after
)
220 def editItem(self
, item
, icon
, name
, comment
, command
, use_term
, parent
=None, final
=True):
221 #if nothing changed don't make a user copy
222 app_info
= item
.get_app_info()
223 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():
225 #hack, item.get_parent() seems to fail a lot
227 parent
= item
.get_parent()
228 self
.writeItem(item
, Icon
=icon
, Name
=name
, Comment
=comment
, Exec
=command
, Terminal
=use_term
)
231 menu_xml
= self
.getXmlMenu(self
.getPath(parent
), dom
.documentElement
, dom
)
232 self
.addXmlTextElement(menu_xml
, 'AppDir', util
.getUserItemPath(), dom
)
235 def editMenu(self
, menu
, icon
, name
, comment
, final
=True):
236 #if nothing changed don't make a user copy
237 if icon
== menu
.get_icon() and name
== menu
.get_name() and comment
== menu
.get_comment():
239 #we don't use this, we just need to make sure the <Menu> exists
240 #otherwise changes won't show up
242 menu_xml
= self
.getXmlMenu(self
.getPath(menu
), dom
.documentElement
, dom
)
243 self
.writeMenu(menu
, Icon
=icon
, Name
=name
, Comment
=comment
)
245 self
.addXmlTextElement(menu_xml
, 'DirectoryDir', util
.getUserDirectoryPath(), dom
)
248 def copyItem(self
, item
, new_parent
, before
=None, after
=None):
250 file_path
= item
.get_desktop_file_path()
251 keyfile
= GLib
.KeyFile()
252 keyfile
.load_from_file(file_path
, util
.KEY_FILE_FLAGS
)
254 util
.fillKeyFile(keyfile
, dict(Categories
=[], Hidden
=False))
256 app_info
= item
.get_app_info()
257 file_id
= util
.getUniqueFileId(app_info
.get_name().replace(os
.sep
, '-'), '.desktop')
258 out_path
= os
.path
.join(util
.getUserItemPath(), file_id
)
260 contents
, length
= keyfile
.to_data()
262 with codecs
.open(out_path
, 'w', 'utf8') as f
:
265 self
.addItem(new_parent
, file_id
, dom
)
266 self
.positionItem(new_parent
, ('Item', file_id
), before
, after
)
270 def deleteItem(self
, item
):
271 self
.writeItem(item
, Hidden
=True)
274 def deleteMenu(self
, menu
):
276 menu_xml
= self
.getXmlMenu(self
.getPath(menu
), dom
.documentElement
, dom
)
277 self
.addDeleted(menu_xml
, dom
)
280 def deleteSeparator(self
, item
):
281 parent
= item
.get_parent()
282 contents
= self
.getContents(parent
)
283 contents
.remove(item
)
284 layout
= self
.createLayout(contents
)
286 menu_xml
= self
.getXmlMenu(self
.getPath(parent
), dom
.documentElement
, dom
)
287 self
.addXmlLayout(menu_xml
, layout
, dom
)
290 def findMenu(self
, menu_id
, parent
=None):
292 parent
= self
.tree
.get_root_directory()
294 if menu_id
== parent
.get_menu_id():
297 item_iter
= parent
.iter()
298 item_type
= item_iter
.next()
299 while item_type
!= GMenu
.TreeItemType
.INVALID
:
300 if item_type
== GMenu
.TreeItemType
.DIRECTORY
:
301 item
= item_iter
.get_directory()
302 if item
.get_menu_id() == menu_id
:
304 menu
= self
.findMenu(menu_id
, item
)
307 item_type
= item_iter
.next()
309 def isVisible(self
, item
):
310 if isinstance(item
, GMenu
.TreeEntry
):
311 app_info
= item
.get_app_info()
312 return not (item
.get_is_excluded() or app_info
.get_nodisplay())
313 elif isinstance(item
, GMenu
.TreeDirectory
):
314 return not item
.get_is_nodisplay()
317 def getPath(self
, menu
):
320 while current
is not None:
321 names
.append(current
.get_menu_id())
322 current
= current
.get_parent()
324 # XXX - don't append root menu name, alacarte doesn't
325 # expect it. look into this more.
329 def getXmlMenuPart(self
, element
, name
):
330 for node
in self
.getXmlNodesByName('Menu', element
):
331 for child
in self
.getXmlNodesByName('Name', node
):
332 if child
.childNodes
[0].nodeValue
== name
:
336 def getXmlMenu(self
, path
, element
, dom
):
338 found
= self
.getXmlMenuPart(element
, name
)
339 if found
is not None:
342 element
= self
.addXmlMenuElement(element
, name
, dom
)
345 def addXmlMenuElement(self
, element
, name
, dom
):
346 node
= dom
.createElement('Menu')
347 self
.addXmlTextElement(node
, 'Name', name
, dom
)
348 return element
.appendChild(node
)
350 def addXmlTextElement(self
, element
, name
, text
, dom
):
351 for temp
in element
.childNodes
:
352 if temp
.nodeName
== name
:
353 if temp
.childNodes
[0].nodeValue
== text
:
355 node
= dom
.createElement(name
)
356 text
= dom
.createTextNode(text
)
357 node
.appendChild(text
)
358 return element
.appendChild(node
)
360 def addXmlFilename(self
, element
, dom
, filename
, type = 'Include'):
361 # remove old filenames
362 for node
in self
.getXmlNodesByName(['Include', 'Exclude'], element
):
363 if node
.childNodes
[0].nodeName
== 'Filename' and node
.childNodes
[0].childNodes
[0].nodeValue
== filename
:
364 element
.removeChild(node
)
367 node
= dom
.createElement(type)
368 node
.appendChild(self
.addXmlTextElement(node
, 'Filename', filename
, dom
))
369 return element
.appendChild(node
)
371 def addDeleted(self
, element
, dom
):
372 node
= dom
.createElement('Deleted')
373 return element
.appendChild(node
)
375 def makeKeyFile(self
, file_path
, kwargs
):
376 if 'KeyFile' in kwargs
:
377 return kwargs
['KeyFile']
379 keyfile
= GLib
.KeyFile()
381 if file_path
is not None:
382 keyfile
.load_from_file(file_path
, util
.KEY_FILE_FLAGS
)
384 util
.fillKeyFile(keyfile
, kwargs
)
387 def writeItem(self
, item
, **kwargs
):
389 file_path
= item
.get_desktop_file_path()
393 keyfile
= self
.makeKeyFile(file_path
, kwargs
)
396 file_id
= item
.get_desktop_file_id()
398 file_id
= util
.getUniqueFileId(keyfile
.get_string(GLib
.KEY_FILE_DESKTOP_GROUP
, 'Name'), '.desktop')
400 contents
, length
= keyfile
.to_data()
402 path
= os
.path
.join(util
.getUserItemPath(), file_id
)
403 with codecs
.open(path
, 'w', 'utf8') as f
:
408 def writeMenu(self
, menu
, **kwargs
):
410 file_id
= os
.path
.split(menu
.get_desktop_file_path())[1]
411 file_path
= menu
.get_desktop_file_path()
412 keyfile
= GLib
.KeyFile()
413 keyfile
.load_from_file(file_path
, util
.KEY_FILE_FLAGS
)
414 elif menu
is None and 'Name' not in kwargs
:
415 raise Exception('New menus need a name')
417 file_id
= util
.getUniqueFileId(kwargs
['Name'], '.directory')
418 keyfile
= GLib
.KeyFile()
420 util
.fillKeyFile(keyfile
, kwargs
)
422 contents
, length
= keyfile
.to_data()
424 path
= os
.path
.join(util
.getUserDirectoryPath(), file_id
)
425 with codecs
.open(path
, 'w', 'utf8') as f
:
429 def getXmlNodesByName(self
, name
, element
):
430 for child
in element
.childNodes
:
431 if child
.nodeType
== xml
.dom
.Node
.ELEMENT_NODE
:
432 if isinstance(name
, str) and child
.nodeName
== name
:
434 elif isinstance(name
, list) or isinstance(name
, tuple):
435 if child
.nodeName
in name
:
438 def addXmlMove(self
, element
, old
, new
, dom
):
439 if not self
.undoMoves(element
, old
, new
, dom
):
440 node
= dom
.createElement('Move')
441 node
.appendChild(self
.addXmlTextElement(node
, 'Old', old
, dom
))
442 node
.appendChild(self
.addXmlTextElement(node
, 'New', new
, dom
))
443 #are parsed in reverse order, need to put at the beginning
444 return element
.insertBefore(node
, element
.firstChild
)
446 def addXmlLayout(self
, element
, layout
, dom
):
448 for node
in self
.getXmlNodesByName('Layout', element
):
449 element
.removeChild(node
)
452 node
= dom
.createElement('Layout')
454 if order
[0] == 'Separator':
455 child
= dom
.createElement('Separator')
456 node
.appendChild(child
)
457 elif order
[0] == 'Filename':
458 child
= self
.addXmlTextElement(node
, 'Filename', order
[1], dom
)
459 elif order
[0] == 'Menuname':
460 child
= self
.addXmlTextElement(node
, 'Menuname', order
[1], dom
)
461 elif order
[0] == 'Merge':
462 child
= dom
.createElement('Merge')
463 child
.setAttribute('type', order
[1])
464 node
.appendChild(child
)
465 return element
.appendChild(node
)
467 def addXmlDefaultLayout(self
, element
, dom
):
468 # remove old default layout
469 for node
in self
.getXmlNodesByName('DefaultLayout', element
):
470 element
.removeChild(node
)
473 node
= dom
.createElement('DefaultLayout')
474 node
.setAttribute('inline', 'false')
475 return element
.appendChild(node
)
477 def createLayout(self
, items
):
479 layout
.append(('Merge', 'menus'))
481 if isinstance(item
, GMenu
.TreeDirectory
):
482 layout
.append(('Menuname', item
.get_menu_id()))
483 elif isinstance(item
, GMenu
.TreeEntry
):
484 layout
.append(('Filename', item
.get_desktop_file_id()))
485 elif isinstance(item
, GMenu
.TreeSeparator
):
486 layout
.append(('Separator',))
489 layout
.append(('Merge', 'files'))
492 def addItem(self
, parent
, file_id
, dom
):
493 xml_parent
= self
.getXmlMenu(self
.getPath(parent
), dom
.documentElement
, dom
)
494 self
.addXmlFilename(xml_parent
, dom
, file_id
, 'Include')
496 def moveItem(self
, parent
, item
, before
=None, after
=None):
497 self
.positionItem(parent
, item
, before
=before
, after
=after
)
500 def positionItem(self
, parent
, item
, before
=None, after
=None):
501 contents
= self
.getContents(parent
)
503 index
= contents
.index(after
) + 1
505 index
= contents
.index(before
)
507 # append the item to the list
508 index
= len(contents
)
509 #if this is a move to a new parent you can't remove the item
511 # decrease the destination index, if we shorten the list
512 if (before
and (contents
.index(item
) < index
)) \
513 or (after
and (contents
.index(item
) < index
- 1)):
515 contents
.remove(item
)
516 contents
.insert(index
, item
)
517 layout
= self
.createLayout(contents
)
519 menu_xml
= self
.getXmlMenu(self
.getPath(parent
), dom
.documentElement
, dom
)
520 self
.addXmlLayout(menu_xml
, layout
, dom
)
522 def undoMoves(self
, element
, old
, new
, dom
):
527 #get all <Move> elements
528 for node
in self
.getXmlNodesByName(['Move'], element
):
529 nodes
.insert(0, node
)
530 #if the <New> matches our old parent we've found a stage to undo
532 xml_old
= node
.getElementsByTagName('Old')[0]
533 xml_new
= node
.getElementsByTagName('New')[0]
534 if xml_new
.childNodes
[0].nodeValue
== old
:
536 #we should end up with this path when completed
537 final_old
= xml_old
.childNodes
[0].nodeValue
540 element
.removeChild(node
)
543 xml_old
= node
.getElementsByTagName('Old')[0]
544 xml_new
= node
.getElementsByTagName('New')[0]
545 path
= os
.path
.split(xml_new
.childNodes
[0].nodeValue
)
546 if path
[0] == original_old
:
547 element
.removeChild(node
)
548 for node
in dom
.getElementsByTagName('Menu'):
549 name_node
= node
.getElementsByTagName('Name')[0]
550 name
= name_node
.childNodes
[0].nodeValue
551 if name
== os
.path
.split(new
)[1]:
552 #copy app and dir directory info from old <Menu>
553 root_path
= dom
.getElementsByTagName('Menu')[0].getElementsByTagName('Name')[0].childNodes
[0].nodeValue
554 xml_menu
= self
.getXmlMenu(root_path
+ '/' + new
, dom
.documentElement
, dom
)
555 for app_dir
in node
.getElementsByTagName('AppDir'):
556 xml_menu
.appendChild(app_dir
)
557 for dir_dir
in node
.getElementsByTagName('DirectoryDir'):
558 xml_menu
.appendChild(dir_dir
)
559 parent
= node
.parentNode
560 parent
.removeChild(node
)
561 node
= dom
.createElement('Move')
562 node
.appendChild(self
.addXmlTextElement(node
, 'Old', xml_old
.childNodes
[0].nodeValue
, dom
))
563 node
.appendChild(self
.addXmlTextElement(node
, 'New', os
.path
.join(new
, path
[1]), dom
))
564 element
.appendChild(node
)
567 node
= dom
.createElement('Move')
568 node
.appendChild(self
.addXmlTextElement(node
, 'Old', final_old
, dom
))
569 node
.appendChild(self
.addXmlTextElement(node
, 'New', new
, dom
))
570 return element
.appendChild(node
)