Use spaces for indents
[alacarte.git] / Alacarte / MenuEditor.py
blob21255ba3c548495a8a53f42d40ea91dc4a91a15e
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
19 import os, sys, re, xml.dom.minidom, locale
20 from gi.repository import GMenu
21 from Alacarte import util
23 class Menu:
24 tree = None
25 visible_tree = None
26 path = None
27 dom = None
29 class MenuEditor:
30 #lists for undo/redo functionality
31 __undo = []
32 __redo = []
34 def __init__(self):
35 self.locale = locale.getdefaultlocale()[0]
36 self.__loadMenus()
38 def reloadMenus(self):
39 self.applications = Menu()
40 self.applications.tree = GMenu.Tree.new('applications.menu', GMenu.TreeFlags.SHOW_EMPTY|GMenu.TreeFlags.INCLUDE_EXCLUDED|GMenu.TreeFlags.INCLUDE_NODISPLAY|GMenu.TreeFlags.SHOW_ALL_SEPARATORS|GMenu.TreeFlags.SORT_DISPLAY_NAME)
41 if not self.applications.tree.load_sync():
42 self.applications = None
43 return
44 self.applications.visible_tree = GMenu.Tree.new('applications.menu', GMenu.TreeFlags.SORT_DISPLAY_NAME)
45 if not self.applications.visible_tree.load_sync():
46 self.applications = None
47 return
48 self.applications.path = os.path.join(util.getUserMenuPath(), self.applications.tree.props.menu_basename)
49 if not os.path.isfile(self.applications.path):
50 self.applications.dom = xml.dom.minidom.parseString(util.getUserMenuXml(self.applications.tree))
51 else:
52 self.applications.dom = xml.dom.minidom.parse(self.applications.path)
53 self.__remove_whilespace_nodes(self.applications.dom)
55 def __menuChanged(self, *a):
56 print >> sys.stderr, "changed!\n"
57 self.applications.visible_tree.load_sync()
59 def __loadMenus(self):
60 self.reloadMenus();
61 self.save(True)
62 self.applications.visible_tree.connect("changed", self.__menuChanged)
64 def save(self, from_loading=False):
65 for menu in ('applications',):
66 fd = open(getattr(self, menu).path, 'w')
67 fd.write(re.sub("\n[\s]*([^\n<]*)\n[\s]*</", "\\1</", getattr(self, menu).dom.toprettyxml().replace('<?xml version="1.0" ?>\n', '')))
68 fd.close()
69 if not from_loading:
70 self.__loadMenus()
72 def quit(self):
73 for file_name in os.listdir(util.getUserItemPath()):
74 if file_name[-6:-2] in ('redo', 'undo'):
75 file_path = os.path.join(util.getUserItemPath(), file_name)
76 os.unlink(file_path)
77 for file_name in os.listdir(util.getUserDirectoryPath()):
78 if file_name[-6:-2] in ('redo', 'undo'):
79 file_path = os.path.join(util.getUserDirectoryPath(), file_name)
80 os.unlink(file_path)
81 for file_name in os.listdir(util.getUserMenuPath()):
82 if file_name[-6:-2] in ('redo', 'undo'):
83 file_path = os.path.join(util.getUserMenuPath(), file_name)
84 os.unlink(file_path)
86 def revert(self):
87 for name in ('applications',):
88 menu = getattr(self, name)
89 self.revertTree(menu.tree.get_root_directory())
90 path = os.path.join(util.getUserMenuPath(), os.path.basename(menu.tree.get_canonical_menu_path()))
91 try:
92 os.unlink(path)
93 except OSError:
94 pass
95 #reload DOM for each menu
96 if not os.path.isfile(menu.path):
97 menu.dom = xml.dom.minidom.parseString(util.getUserMenuXml(menu.tree))
98 else:
99 menu.dom = xml.dom.minidom.parse(menu.path)
100 self.__remove_whilespace_nodes(menu.dom)
101 #reset undo/redo, no way to recover from this
102 self.__undo, self.__redo = [], []
103 self.save()
105 def revertTree(self, menu):
106 item_iter = menu.iter()
107 item_type = item_iter.next()
108 while item_type != GMenu.TreeItemType.INVALID:
109 if item_type == GMenu.TreeItemType.DIRECTORY:
110 item = item_iter.get_directory();
111 self.revertTree(item)
112 elif item_type == GMenu.TreeItemType.ENTRY:
113 item = item_iter.get_entry();
114 self.revertItem(item)
115 item_type = item_iter.next()
116 self.revertMenu(menu)
118 def undo(self):
119 if len(self.__undo) == 0:
120 return
121 files = self.__undo.pop()
122 redo = []
123 for file_path in files:
124 new_path = file_path.rsplit('.', 1)[0]
125 redo_path = util.getUniqueRedoFile(new_path)
126 data = open(new_path).read()
127 open(redo_path, 'w').write(data)
128 data = open(file_path).read()
129 open(new_path, 'w').write(data)
130 os.unlink(file_path)
131 redo.append(redo_path)
132 #reload DOM to make changes stick
133 for name in ('applications',):
134 menu = getattr(self, name)
135 if not os.path.isfile(menu.path):
136 menu.dom = xml.dom.minidom.parseString(util.getUserMenuXml(menu.tree))
137 else:
138 menu.dom = xml.dom.minidom.parse(menu.path)
139 self.__remove_whilespace_nodes(menu.dom)
140 self.__redo.append(redo)
142 def redo(self):
143 if len(self.__redo) == 0:
144 return
145 files = self.__redo.pop()
146 undo = []
147 for file_path in files:
148 new_path = file_path.rsplit('.', 1)[0]
149 undo_path = util.getUniqueUndoFile(new_path)
150 data = open(new_path).read()
151 open(undo_path, 'w').write(data)
152 data = open(file_path).read()
153 open(new_path, 'w').write(data)
154 os.unlink(file_path)
155 undo.append(undo_path)
156 #reload DOM to make changes stick
157 for name in ('applications',):
158 menu = getattr(self, name)
159 if not os.path.isfile(menu.path):
160 menu.dom = xml.dom.minidom.parseString(util.getUserMenuXml(menu.tree))
161 else:
162 menu.dom = xml.dom.minidom.parse(menu.path)
163 self.__remove_whilespace_nodes(menu.dom)
164 self.__undo.append(undo)
166 def getMenus(self, parent=None):
167 if parent == None:
168 yield self.applications.tree.get_root_directory()
169 else:
170 item_iter = parent.iter()
171 item_type = item_iter.next()
172 while item_type != GMenu.TreeItemType.INVALID:
173 if item_type == GMenu.TreeItemType.DIRECTORY:
174 item = item_iter.get_directory()
175 yield (item, self.__isVisible(item))
176 item_type = item_iter.next()
178 def getContents(self, item):
179 contents = []
180 item_iter = item.iter()
181 item_type = item_iter.next()
183 while item_type != GMenu.TreeItemType.INVALID:
184 if item_type == GMenu.TreeItemType.DIRECTORY:
185 item = item_iter.get_directory()
186 elif item_type == GMenu.TreeItemType.ENTRY:
187 item = item_iter.get_entry()
188 elif item_type == GMenu.TreeItemType.HEADER:
189 item = item_iter.get_header()
190 elif item_type == GMenu.TreeItemType.ALIAS:
191 item = item_iter.get_alias()
192 if item:
193 contents.append(item)
194 item_type = item_iter.next()
195 return contents;
197 def getItems(self, menu):
198 item_iter = menu.iter()
199 item_type = item_iter.next()
200 while item_type != GMenu.TreeItemType.INVALID:
201 if item_type == GMenu.TreeItemType.SEPARATOR:
202 item = item_iter.get_separator()
203 yield (item, True)
204 else:
205 if item_type == GMenu.TreeItemType.ENTRY:
206 item = item_iter.get_entry()
207 if item.get_desktop_file_id()[-19:] == '-usercustom.desktop':
208 continue
209 elif item_type == GMenu.TreeItemType.DIRECTORY:
210 item = item_iter.get_directory()
211 elif item_type == GMenu.TreeItemType.HEADER:
212 item = item_iter.get_header()
213 elif item_type == GMenu.TreeItemType.ALIAS:
214 item = item_iter.get_alias()
215 yield (item, self.__isVisible(item))
216 item_type = item_iter.next()
218 def canRevert(self, item):
219 if isinstance(item, GMenu.TreeEntry):
220 if util.getItemPath(item.get_desktop_file_id()):
221 path = util.getUserItemPath()
222 if os.path.isfile(os.path.join(path, item.get_desktop_file_id())):
223 return True
224 elif isinstance(item, GMenu.TreeDirectory):
225 if item.get_desktop_file_path():
226 file_id = os.path.split(item.get_desktop_file_path())[1]
227 else:
228 file_id = item.get_menu_id() + '.directory'
229 if util.getDirectoryPath(file_id):
230 path = util.getUserDirectoryPath()
231 if os.path.isfile(os.path.join(path, file_id)):
232 return True
233 return False
235 def setVisible(self, item, visible):
236 dom = self.__getMenu(item).dom
237 if isinstance(item, GMenu.TreeEntry):
238 self.__addUndo([self.__getMenu(item), item])
239 menu_xml = self.__getXmlMenu(self.__getPath(item.get_parent()), dom, dom)
240 if visible:
241 self.__addXmlFilename(menu_xml, dom, item.get_desktop_file_id(), 'Include')
242 self.__writeItem(item, no_display=False)
243 else:
244 self.__addXmlFilename(menu_xml, dom, item.get_desktop_file_id(), 'Exclude')
245 self.__addXmlTextElement(menu_xml, 'AppDir', util.getUserItemPath(), dom)
246 elif isinstance(item, GMenu.TreeDirectory):
247 self.__addUndo([self.__getMenu(item), item])
248 item_iter = item.iter()
249 first_child_type = item_iter.next()
250 #don't mess with it if it's empty
251 if first_child_type == GMenu.TreeItemType.INVALID:
252 return
253 menu_xml = self.__getXmlMenu(self.__getPath(item), dom, dom)
254 for node in self.__getXmlNodesByName(['Deleted', 'NotDeleted'], menu_xml):
255 node.parentNode.removeChild(node)
256 if visible:
257 self.__writeMenu(item, no_display=False)
258 else:
259 self.__writeMenu(item, no_display=True)
260 self.__addXmlTextElement(menu_xml, 'DirectoryDir', util.getUserDirectoryPath(), dom)
261 self.save()
263 def createItem(self, parent, icon, name, comment, command, use_term, before=None, after=None):
264 file_id = self.__writeItem(None, icon, name, comment, command, use_term)
265 self.insertExternalItem(file_id, parent.get_menu_id(), before, after)
267 def insertExternalItem(self, file_id, parent_id, before=None, after=None):
268 self.applications.tree.get_root_directory()
269 parent = self.__findMenu(parent_id)
270 self.applications.tree.get_root_directory()
271 dom = self.__getMenu(parent).dom
272 self.applications.tree.get_root_directory()
273 self.__addItem(parent, file_id, dom)
274 self.applications.tree.get_root_directory()
275 self.__positionItem(parent, ('Item', file_id), before, after)
276 self.applications.tree.get_root_directory()
277 self.__addUndo([self.__getMenu(parent), ('Item', file_id)])
278 self.applications.tree.get_root_directory()
279 self.save()
280 self.applications.tree.get_root_directory()
282 def createMenu(self, parent, icon, name, comment, before=None, after=None):
283 file_id = self.__writeMenu(None, icon, name, comment)
284 self.insertExternalMenu(file_id, parent.get_menu_id(), before, after)
286 def insertExternalMenu(self, file_id, parent_id, before=None, after=None):
287 menu_id = file_id.rsplit('.', 1)[0]
288 parent = self.__findMenu(parent_id)
289 dom = self.__getMenu(parent).dom
290 self.__addXmlDefaultLayout(self.__getXmlMenu(self.__getPath(parent), dom, dom) , dom)
291 menu_xml = self.__getXmlMenu(self.__getPath(parent) + '/' + menu_id, dom, dom)
292 self.__addXmlTextElement(menu_xml, 'Directory', file_id, dom)
293 self.__positionItem(parent, ('Menu', menu_id), before, after)
294 self.__addUndo([self.__getMenu(parent), ('Menu', file_id)])
295 self.save()
297 def createSeparator(self, parent, before=None, after=None):
298 self.__positionItem(parent, ('Separator',), before, after)
299 self.__addUndo([self.__getMenu(parent), ('Separator',)])
300 self.save()
302 def editItem(self, item, icon, name, comment, command, use_term, parent=None, final=True):
303 #if nothing changed don't make a user copy
304 app_info = item.get_app_info()
305 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():
306 return
307 #hack, item.get_parent() seems to fail a lot
308 if not parent:
309 parent = item.get_parent()
310 if final:
311 self.__addUndo([self.__getMenu(parent), item])
312 self.__writeItem(item, icon, name, comment, command, use_term)
313 if final:
314 dom = self.__getMenu(parent).dom
315 menu_xml = self.__getXmlMenu(self.__getPath(parent), dom, dom)
316 self.__addXmlTextElement(menu_xml, 'AppDir', util.getUserItemPath(), dom)
317 self.save()
319 def editMenu(self, menu, icon, name, comment, final=True):
320 #if nothing changed don't make a user copy
321 if icon == menu.get_icon() and name == menu.get_name() and comment == menu.get_comment():
322 return
323 #we don't use this, we just need to make sure the <Menu> exists
324 #otherwise changes won't show up
325 dom = self.__getMenu(menu).dom
326 menu_xml = self.__getXmlMenu(self.__getPath(menu), dom, dom)
327 file_id = self.__writeMenu(menu, icon, name, comment)
328 if final:
329 self.__addXmlTextElement(menu_xml, 'DirectoryDir', util.getUserDirectoryPath(), dom)
330 self.__addUndo([self.__getMenu(menu), menu])
331 self.save()
333 def copyItem(self, item, new_parent, before=None, after=None):
334 dom = self.__getMenu(new_parent).dom
335 file_path = item.get_desktop_file_path()
336 keyfile = util.DesktopParser(file_path)
337 #erase Categories in new file
338 keyfile.set('Categories', ('',))
339 keyfile.set('Hidden', False)
340 app_info = item.get_info()
341 file_id = util.getUniqueFileId(app_info.get_name().replace(os.sep, '-'), '.desktop')
342 out_path = os.path.join(util.getUserItemPath(), file_id)
343 keyfile.write(open(out_path, 'w'))
344 self.__addItem(new_parent, file_id, dom)
345 self.__positionItem(new_parent, ('Item', file_id), before, after)
346 self.__addUndo([self.__getMenu(new_parent), ('Item', file_id)])
347 self.save()
348 return file_id
350 def moveItem(self, item, new_parent, before=None, after=None):
351 undo = []
352 if item.get_parent() != new_parent:
353 #hide old item
354 self.deleteItem(item)
355 undo.append(item)
356 file_id = self.copyItem(item, new_parent)
357 item = ('Item', file_id)
358 undo.append(item)
359 self.__positionItem(new_parent, item, before, after)
360 undo.append(self.__getMenu(new_parent))
361 self.__addUndo(undo)
362 self.save()
364 def moveMenu(self, menu, new_parent, before=None, after=None):
365 parent = new_parent
366 #don't move a menu into it's child
367 while parent.get_parent():
368 parent = parent.get_parent()
369 if parent == menu:
370 return False
372 #don't move a menu into itself
373 if new_parent == menu:
374 return False
376 #can't move between top-level menus
377 if self.__getMenu(menu) != self.__getMenu(new_parent):
378 return False
379 if menu.get_parent() != new_parent:
380 dom = self.__getMenu(menu).dom
381 root_path = self.__getPath(menu).split('/', 1)[0]
382 xml_root = self.__getXmlMenu(root_path, dom, dom)
383 old_path = self.__getPath(menu).split('/', 1)[1]
384 #root menu's path has no /
385 if '/' in self.__getPath(new_parent):
386 new_path = self.__getPath(new_parent).split('/', 1)[1] + '/' + menu.get_menu_id()
387 else:
388 new_path = menu.get_menu_id()
389 self.__addXmlMove(xml_root, old_path, new_path, dom)
390 self.__positionItem(new_parent, menu, before, after)
391 self.__addUndo([self.__getMenu(new_parent),])
392 self.save()
394 def moveSeparator(self, separator, new_parent, before=None, after=None):
395 undo = []
396 # remove the original separator if its parent is not the new destination
397 if separator.get_parent() != new_parent:
398 self.deleteSeparator(separator)
399 undo.append(separator)
400 # this adds the new separator to the specified position
401 self.__positionItem(new_parent, separator, before, after)
402 undo.append(self.__getMenu(new_parent))
403 self.__addUndo(undo)
404 self.save()
406 def deleteItem(self, item):
407 self.__addUndo([item,])
408 self.__writeItem(item, hidden=True)
409 self.save()
411 def deleteMenu(self, menu):
412 dom = self.__getMenu(menu).dom
413 menu_xml = self.__getXmlMenu(self.__getPath(menu), dom, dom)
414 self.__addDeleted(menu_xml, dom)
415 self.__addUndo([self.__getMenu(menu),])
416 self.save()
418 def deleteSeparator(self, item):
419 parent = item.get_parent()
420 contents = self.getContents(parent)
421 contents.remove(item)
422 layout = self.__createLayout(contents)
423 dom = self.__getMenu(parent).dom
424 menu_xml = self.__getXmlMenu(self.__getPath(parent), dom, dom)
425 self.__addXmlLayout(menu_xml, layout, dom)
426 self.__addUndo([self.__getMenu(item.get_parent()),])
427 self.save()
429 def revertItem(self, item):
430 if not self.canRevert(item):
431 return
432 self.__addUndo([item,])
433 try:
434 os.remove(item.get_desktop_file_path())
435 except OSError:
436 pass
437 self.save()
439 def revertMenu(self, menu):
440 if not self.canRevert(menu):
441 return
442 #wtf happened here? oh well, just bail
443 if not menu.get_desktop_file_path():
444 return
445 self.__addUndo([menu,])
446 file_id = os.path.split(menu.get_desktop_file_path())[1]
447 path = os.path.join(util.getUserDirectoryPath(), file_id)
448 try:
449 os.remove(path)
450 except OSError:
451 pass
452 self.save()
454 #private stuff
455 def __addUndo(self, items):
456 self.__undo.append([])
457 for item in items:
458 if isinstance(item, Menu):
459 file_path = item.path
460 elif isinstance(item, tuple):
461 if item[0] == 'Item':
462 file_path = os.path.join(util.getUserItemPath(), item[1])
463 if not os.path.isfile(file_path):
464 file_path = util.getItemPath(item[1])
465 elif item[0] == 'Menu':
466 file_path = os.path.join(util.getUserDirectoryPath(), item[1])
467 if not os.path.isfile(file_path):
468 file_path = util.getDirectoryPath(item[1])
469 else:
470 continue
471 elif isinstance(item, GMenu.TreeDirectory):
472 if item.get_desktop_file_path() == None:
473 continue
474 file_path = os.path.join(util.getUserDirectoryPath(), os.path.split(item.get_desktop_file_path())[1])
475 if not os.path.isfile(file_path):
476 file_path = item.get_desktop_file_path()
477 elif isinstance(item, GMenu.TreeEntry):
478 file_path = os.path.join(util.getUserItemPath(), item.get_desktop_file_id())
479 if not os.path.isfile(file_path):
480 file_path = item.get_desktop_file_path()
481 else:
482 continue
483 data = open(file_path).read()
484 undo_path = util.getUniqueUndoFile(file_path)
485 open(undo_path, 'w').write(data)
486 self.__undo[-1].append(undo_path)
488 def __getMenu(self, item):
489 return self.applications
491 def __findMenu(self, menu_id, parent=None):
492 root_directory = self.applications.tree.get_root_directory()
493 if parent == None and root_directory != None:
494 return self.__findMenu(menu_id, root_directory)
495 if menu_id == root_directory.get_menu_id():
496 return root_directory
497 item_iter = parent.iter()
498 item_type = item_iter.next()
499 while item_type != GMenu.TreeItemType.INVALID:
500 if item_type == GMenu.TreeItemType.DIRECTORY:
501 item = item_iter.get_directory()
502 if item.get_menu_id() == menu_id:
503 return item
504 menu = self.__findMenu(menu_id, item)
505 if menu != None:
506 return menu
507 item_type = item_iter.next()
509 def __isVisible(self, item):
510 if isinstance(item, GMenu.TreeEntry):
511 app_info = item.get_app_info()
512 return not (item.get_is_excluded() or app_info.get_nodisplay())
513 menu = self.__getMenu(item)
514 if menu == self.applications:
515 root = self.applications.visible_tree.get_root_directory()
516 if isinstance(item, GMenu.TreeDirectory):
517 if self.__findMenu(item.get_menu_id(), root) == None:
518 return False
519 return True
521 def __getPath(self, menu, path=None):
522 if not path:
523 path = menu.get_menu_id()
524 if menu.get_parent():
525 path = self.__getPath(menu.get_parent(), path)
526 path += '/'
527 path += menu.get_menu_id()
528 print "%s\n" % path
529 return path
531 def __getXmlMenu(self, path, element, dom):
532 if '/' in path:
533 (name, path) = path.split('/', 1)
534 else:
535 name = path
536 path = ''
538 found = None
539 for node in self.__getXmlNodesByName('Menu', element):
540 for child in self.__getXmlNodesByName('Name', node):
541 if child.childNodes[0].nodeValue == name:
542 if path:
543 found = self.__getXmlMenu(path, node, dom)
544 else:
545 found = node
546 break
547 if found:
548 break
549 if not found:
550 node = self.__addXmlMenuElement(element, name, dom)
551 if path:
552 found = self.__getXmlMenu(path, node, dom)
553 else:
554 found = node
556 return found
558 def __addXmlMenuElement(self, element, name, dom):
559 node = dom.createElement('Menu')
560 self.__addXmlTextElement(node, 'Name', name, dom)
561 return element.appendChild(node)
563 def __addXmlTextElement(self, element, name, text, dom):
564 for temp in element.childNodes:
565 if temp.nodeName == name:
566 if temp.childNodes[0].nodeValue == text:
567 return
568 node = dom.createElement(name)
569 text = dom.createTextNode(text)
570 node.appendChild(text)
571 return element.appendChild(node)
573 def __addXmlFilename(self, element, dom, filename, type = 'Include'):
574 # remove old filenames
575 for node in self.__getXmlNodesByName(['Include', 'Exclude'], element):
576 if node.childNodes[0].nodeName == 'Filename' and node.childNodes[0].childNodes[0].nodeValue == filename:
577 element.removeChild(node)
579 # add new filename
580 node = dom.createElement(type)
581 node.appendChild(self.__addXmlTextElement(node, 'Filename', filename, dom))
582 return element.appendChild(node)
584 def __addDeleted(self, element, dom):
585 node = dom.createElement('Deleted')
586 return element.appendChild(node)
588 def __writeItem(self, item=None, icon=None, name=None, comment=None, command=None, use_term=None, no_display=None, startup_notify=None, hidden=None):
589 if item:
590 file_path = item.get_desktop_file_path()
591 file_id = item.get_desktop_file_id()
592 keyfile = util.DesktopParser(file_path)
593 elif item == None and name == None:
594 raise Exception('New menu items need a name')
595 else:
596 file_id = util.getUniqueFileId(name, '.desktop')
597 keyfile = util.DesktopParser()
598 if icon:
599 keyfile.set('Icon', icon)
600 keyfile.set('Icon', icon, self.locale)
601 if name:
602 keyfile.set('Name', name)
603 keyfile.set('Name', name, self.locale)
604 if comment:
605 keyfile.set('Comment', comment)
606 keyfile.set('Comment', comment, self.locale)
607 if command:
608 keyfile.set('Exec', command)
609 if use_term != None:
610 keyfile.set('Terminal', use_term)
611 if no_display != None:
612 keyfile.set('NoDisplay', no_display)
613 if startup_notify != None:
614 keyfile.set('StartupNotify', startup_notify)
615 if hidden != None:
616 keyfile.set('Hidden', hidden)
617 out_path = os.path.join(util.getUserItemPath(), file_id)
618 keyfile.write(open(out_path, 'w'))
619 return file_id
621 def __writeMenu(self, menu=None, icon=None, name=None, comment=None, no_display=None):
622 if menu:
623 file_id = os.path.split(menu.get_desktop_file_path())[1]
624 file_path = menu.get_desktop_file_path()
625 keyfile = util.DesktopParser(file_path)
626 elif menu == None and name == None:
627 raise Exception('New menus need a name')
628 else:
629 file_id = util.getUniqueFileId(name, '.directory')
630 keyfile = util.DesktopParser(file_type='Directory')
631 if icon:
632 keyfile.set('Icon', icon)
633 if name:
634 keyfile.set('Name', name)
635 keyfile.set('Name', name, self.locale)
636 if comment:
637 keyfile.set('Comment', comment)
638 keyfile.set('Comment', comment, self.locale)
639 if no_display != None:
640 keyfile.set('NoDisplay', no_display)
641 out_path = os.path.join(util.getUserDirectoryPath(), file_id)
642 keyfile.write(open(out_path, 'w'))
643 return file_id
645 def __getXmlNodesByName(self, name, element):
646 for child in element.childNodes:
647 if child.nodeType == xml.dom.Node.ELEMENT_NODE:
648 if isinstance(name, str) and child.nodeName == name:
649 yield child
650 elif isinstance(name, list) or isinstance(name, tuple):
651 if child.nodeName in name:
652 yield child
654 def __remove_whilespace_nodes(self, node):
655 remove_list = []
656 for child in node.childNodes:
657 if child.nodeType == xml.dom.minidom.Node.TEXT_NODE:
658 child.data = child.data.strip()
659 if not child.data.strip():
660 remove_list.append(child)
661 elif child.hasChildNodes():
662 self.__remove_whilespace_nodes(child)
663 for node in remove_list:
664 node.parentNode.removeChild(node)
666 def __addXmlMove(self, element, old, new, dom):
667 if not self.__undoMoves(element, old, new, dom):
668 node = dom.createElement('Move')
669 node.appendChild(self.__addXmlTextElement(node, 'Old', old, dom))
670 node.appendChild(self.__addXmlTextElement(node, 'New', new, dom))
671 #are parsed in reverse order, need to put at the beginning
672 return element.insertBefore(node, element.firstChild)
674 def __addXmlLayout(self, element, layout, dom):
675 # remove old layout
676 for node in self.__getXmlNodesByName('Layout', element):
677 element.removeChild(node)
679 # add new layout
680 node = dom.createElement('Layout')
681 for order in layout.order:
682 if order[0] == 'Separator':
683 child = dom.createElement('Separator')
684 node.appendChild(child)
685 elif order[0] == 'Filename':
686 child = self.__addXmlTextElement(node, 'Filename', order[1], dom)
687 elif order[0] == 'Menuname':
688 child = self.__addXmlTextElement(node, 'Menuname', order[1], dom)
689 elif order[0] == 'Merge':
690 child = dom.createElement('Merge')
691 child.setAttribute('type', order[1])
692 node.appendChild(child)
693 return element.appendChild(node)
695 def __addXmlDefaultLayout(self, element, dom):
696 # remove old default layout
697 for node in self.__getXmlNodesByName('DefaultLayout', element):
698 element.removeChild(node)
700 # add new layout
701 node = dom.createElement('DefaultLayout')
702 node.setAttribute('inline', 'false')
703 return element.appendChild(node)
705 def __createLayout(self, items):
706 layout = Layout()
707 layout.order = []
709 layout.order.append(['Merge', 'menus'])
710 for item in items:
711 if isinstance(item, tuple):
712 if item[0] == 'Separator':
713 layout.parseSeparator()
714 elif item[0] == 'Menu':
715 layout.parseMenuname(item[1])
716 elif item[0] == 'Item':
717 layout.parseFilename(item[1])
718 elif isinstance(item, GMenu.TreeDirectory):
719 layout.parseMenuname(item.get_menu_id())
720 elif isinstance(item, GMenu.TreeEntry):
721 layout.parseFilename(item.get_desktop_file_id())
722 elif isinstance(item, GMenu.TreeSeparator):
723 layout.parseSeparator()
724 layout.order.append(['Merge', 'files'])
725 return layout
727 def __addItem(self, parent, file_id, dom):
728 xml_parent = self.__getXmlMenu(self.__getPath(parent), dom, dom)
729 self.__addXmlFilename(xml_parent, dom, file_id, 'Include')
731 def __deleteItem(self, parent, file_id, dom, before=None, after=None):
732 xml_parent = self.__getXmlMenu(self.__getPath(parent), dom, dom)
733 self.__addXmlFilename(xml_parent, dom, file_id, 'Exclude')
735 def __positionItem(self, parent, item, before=None, after=None):
736 contents = self.getContents(parent)
737 if after:
738 index = contents.index(after) + 1
739 elif before:
740 index = contents.index(before)
741 else:
742 # append the item to the list
743 index = len(contents)
744 #if this is a move to a new parent you can't remove the item
745 if item in contents:
746 # decrease the destination index, if we shorten the list
747 if (before and (contents.index(item) < index)) \
748 or (after and (contents.index(item) < index - 1)):
749 index -= 1
750 contents.remove(item)
751 contents.insert(index, item)
752 layout = self.__createLayout(contents)
753 dom = self.__getMenu(parent).dom
754 menu_xml = self.__getXmlMenu(self.__getPath(parent), dom, dom)
755 self.__addXmlLayout(menu_xml, layout, dom)
757 def __undoMoves(self, element, old, new, dom):
758 nodes = []
759 matches = []
760 original_old = old
761 final_old = old
762 #get all <Move> elements
763 for node in self.__getXmlNodesByName(['Move'], element):
764 nodes.insert(0, node)
765 #if the <New> matches our old parent we've found a stage to undo
766 for node in nodes:
767 xml_old = node.getElementsByTagName('Old')[0]
768 xml_new = node.getElementsByTagName('New')[0]
769 if xml_new.childNodes[0].nodeValue == old:
770 matches.append(node)
771 #we should end up with this path when completed
772 final_old = xml_old.childNodes[0].nodeValue
773 #undoing <Move>s
774 for node in matches:
775 element.removeChild(node)
776 if len(matches) > 0:
777 for node in nodes:
778 xml_old = node.getElementsByTagName('Old')[0]
779 xml_new = node.getElementsByTagName('New')[0]
780 path = os.path.split(xml_new.childNodes[0].nodeValue)
781 if path[0] == original_old:
782 element.removeChild(node)
783 for node in dom.getElementsByTagName('Menu'):
784 name_node = node.getElementsByTagName('Name')[0]
785 name = name_node.childNodes[0].nodeValue
786 if name == os.path.split(new)[1]:
787 #copy app and dir directory info from old <Menu>
788 root_path = dom.getElementsByTagName('Menu')[0].getElementsByTagName('Name')[0].childNodes[0].nodeValue
789 xml_menu = self.__getXmlMenu(root_path + '/' + new, dom, dom)
790 for app_dir in node.getElementsByTagName('AppDir'):
791 xml_menu.appendChild(app_dir)
792 for dir_dir in node.getElementsByTagName('DirectoryDir'):
793 xml_menu.appendChild(dir_dir)
794 parent = node.parentNode
795 parent.removeChild(node)
796 node = dom.createElement('Move')
797 node.appendChild(self.__addXmlTextElement(node, 'Old', xml_old.childNodes[0].nodeValue, dom))
798 node.appendChild(self.__addXmlTextElement(node, 'New', os.path.join(new, path[1]), dom))
799 element.appendChild(node)
800 if final_old == new:
801 return True
802 node = dom.createElement('Move')
803 node.appendChild(self.__addXmlTextElement(node, 'Old', final_old, dom))
804 node.appendChild(self.__addXmlTextElement(node, 'New', new, dom))
805 return element.appendChild(node)
807 class Layout:
808 def __init__(self, node=None):
809 self.order = []
811 def parseMenuname(self, value):
812 self.order.append(['Menuname', value])
814 def parseSeparator(self):
815 self.order.append(['Separator'])
817 def parseFilename(self, value):
818 self.order.append(['Filename', value])
820 def parseMerge(self, merge_type='all'):
821 self.order.append(['Merge', merge_type])