toolboxes.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. """!
  2. @package core.toolboxes
  3. @brief Functions for modifying menu from default/user toolboxes specified in XML files
  4. (C) 2013 by the GRASS Development Team
  5. This program is free software under the GNU General Public License
  6. (>=v2). Read the file COPYING that comes with GRASS for details.
  7. @author Vaclav Petras <wenzeslaus gmail.com>
  8. @author Anna Petrasova <kratochanna gmail.com>
  9. """
  10. import os
  11. import sys
  12. import copy
  13. import xml.etree.ElementTree as etree
  14. from xml.parsers import expat
  15. # Get the XML parsing exceptions to catch. The behavior chnaged with Python 2.7
  16. # and ElementTree 1.3.
  17. if hasattr(etree, 'ParseError'):
  18. ETREE_EXCEPTIONS = (etree.ParseError, expat.ExpatError)
  19. else:
  20. ETREE_EXCEPTIONS = (expat.ExpatError)
  21. if sys.version_info[0:2] > (2, 6):
  22. has_xpath = True
  23. else:
  24. has_xpath = False
  25. if __name__ == '__main__':
  26. sys.path.append(os.path.join(os.environ['GISBASE'], "etc", "gui", "wxpython"))
  27. from core.globalvar import ETCWXDIR
  28. from core.utils import GetSettingsPath
  29. from core.gcmd import GError
  30. import grass.script.task as gtask
  31. from grass.script.core import ScriptError
  32. mainMenuFile = os.path.join(ETCWXDIR, 'xml', 'main_menu.xml')
  33. toolboxesFile = os.path.join(ETCWXDIR, 'xml', 'toolboxes.xml')
  34. wxguiItemsFile = os.path.join(ETCWXDIR, 'xml', 'wxgui_items.xml')
  35. moduleItemsFile = os.path.join(ETCWXDIR, 'xml', 'module_items.xml')
  36. userToolboxesFile = os.path.join(GetSettingsPath(), 'toolboxes', 'toolboxes.xml')
  37. userMainMenuFile = os.path.join(GetSettingsPath(), 'toolboxes', 'main_menu.xml')
  38. if not os.path.exists(userToolboxesFile):
  39. userToolboxesFile = None
  40. if not os.path.exists(userMainMenuFile):
  41. userMainMenuFile = None
  42. def getMenuFile():
  43. """!Returns path to XML file for building menu.
  44. Creates toolbox directory where user defined toolboxes should be located.
  45. Checks whether it is needed to create new XML file (user changed toolboxes)
  46. or the already generated file could be used.
  47. If something goes wrong during building or user doesn't modify menu,
  48. default file (from distribution) is returned.
  49. """
  50. fallback = os.path.join(ETCWXDIR, 'xml', 'menudata.xml')
  51. # always create toolboxes directory if does not exist yet
  52. tbDir = _setupToolboxes()
  53. if tbDir:
  54. menudataFile = os.path.join(tbDir, 'menudata.xml')
  55. generateNew = False
  56. # when any of main_menu.xml or toolboxes.xml are changed,
  57. # generate new menudata.xml
  58. if os.path.exists(menudataFile):
  59. # remove menu file when there is no main_menu and toolboxes
  60. if not userToolboxesFile and not userMainMenuFile:
  61. os.remove(menudataFile)
  62. return fallback
  63. if bool(userToolboxesFile) != bool(userMainMenuFile):
  64. # always generate new because we don't know if there has been any change
  65. generateNew = True
  66. else:
  67. # if newer files -> generate new
  68. menudataTime = os.path.getmtime(menudataFile)
  69. if userToolboxesFile:
  70. if os.path.getmtime(userToolboxesFile) > menudataTime:
  71. generateNew = True
  72. if userMainMenuFile:
  73. if os.path.getmtime(userMainMenuFile) > menudataTime:
  74. generateNew = True
  75. elif userToolboxesFile or userMainMenuFile:
  76. generateNew = True
  77. else:
  78. return fallback
  79. if generateNew:
  80. try:
  81. tree = toolboxes2menudata()
  82. except ETREE_EXCEPTIONS:
  83. GError(_("Unable to parse user toolboxes XML files. "
  84. "Default toolboxes will be loaded."))
  85. return fallback
  86. try:
  87. xml = _getXMLString(tree.getroot())
  88. fh = open(os.path.join(tbDir, 'menudata.xml'), 'w')
  89. fh.write(xml)
  90. fh.close()
  91. return menudataFile
  92. except:
  93. return fallback
  94. else:
  95. return menudataFile
  96. else:
  97. return fallback
  98. def _setupToolboxes():
  99. """!Create 'toolboxes' directory if doesn't exist."""
  100. path = os.path.join(GetSettingsPath(), 'toolboxes')
  101. if not os.path.exists(path):
  102. try:
  103. os.mkdir(path)
  104. except:
  105. GError(_('Unable to create toolboxes directory.'))
  106. return None
  107. return path
  108. def toolboxes2menudata(userDefined=True):
  109. """!Creates XML file with data for menu.
  110. Parses toolboxes files from distribution and from users,
  111. puts them together, adds metadata to modules and convert
  112. tree to previous format used for loading menu.
  113. @param userDefined use toolboxes defined by user or not (during compilation)
  114. @return ElementTree instance
  115. """
  116. wxguiItems = etree.parse(wxguiItemsFile)
  117. moduleItems = etree.parse(moduleItemsFile)
  118. if userDefined and userMainMenuFile:
  119. mainMenu = etree.parse(userMainMenuFile)
  120. else:
  121. mainMenu = etree.parse(mainMenuFile)
  122. root = mainMenu.getroot()
  123. if userDefined and userToolboxesFile:
  124. userToolboxes = etree.parse(userToolboxesFile)
  125. _expandUserToolboxesItem(root, userToolboxes)
  126. _expandToolboxes(root, userToolboxes)
  127. if not userToolboxesFile:
  128. _removeUserToolboxesItem(root)
  129. toolboxes = etree.parse(toolboxesFile)
  130. _expandToolboxes(root, toolboxes)
  131. _expandItems(root, moduleItems, 'module-item')
  132. _expandItems(root, wxguiItems, 'wxgui-item')
  133. # in case of compilation there are no additional runtime modules
  134. # but we need to create empty elements
  135. _expandRuntimeModules(root)
  136. _addHandlers(root)
  137. _convertTree(root)
  138. _indent(root)
  139. return mainMenu
  140. def _indent(elem, level=0):
  141. """!Helper function to fix indentation of XML files."""
  142. i = "\n" + level * " "
  143. if len(elem):
  144. if not elem.text or not elem.text.strip():
  145. elem.text = i + " "
  146. if not elem.tail or not elem.tail.strip():
  147. elem.tail = i
  148. for elem in elem:
  149. _indent(elem, level + 1)
  150. if not elem.tail or not elem.tail.strip():
  151. elem.tail = i
  152. else:
  153. if level and (not elem.tail or not elem.tail.strip()):
  154. elem.tail = i
  155. def _expandToolboxes(node, toolboxes):
  156. """!Expands tree with toolboxes.
  157. Function is called recursively.
  158. @param node tree node where to look for subtoolboxes to be expanded
  159. @param toolboxes tree of toolboxes to be used for expansion
  160. """
  161. nodes = node.findall('.//toolbox')
  162. if node.tag == 'toolbox': # root
  163. nodes.append(node)
  164. for n in nodes:
  165. if n.find('items') is None:
  166. continue
  167. for subtoolbox in n.findall('./items/subtoolbox'):
  168. items = n.find('./items')
  169. idx = items.getchildren().index(subtoolbox)
  170. if has_xpath:
  171. toolbox = toolboxes.find('.//toolbox[@name="%s"]' % subtoolbox.get('name'))
  172. else:
  173. toolbox = None
  174. potentialToolboxes = toolboxes.findall('.//toolbox')
  175. sName = subtoolbox.get('name')
  176. for pToolbox in potentialToolboxes:
  177. if pToolbox.get('name') == sName:
  178. toolbox = pToolbox
  179. break
  180. if toolbox is None: # not in file
  181. continue
  182. _expandToolboxes(toolbox, toolboxes)
  183. items.insert(idx, toolbox)
  184. items.remove(subtoolbox)
  185. def _expandUserToolboxesItem(node, toolboxes):
  186. """!Expand tag 'user-toolboxes-list'.
  187. Include all user toolboxes.
  188. """
  189. tboxes = toolboxes.findall('.//toolbox')
  190. for n in node.findall('./items/user-toolboxes-list'):
  191. items = node.find('./items')
  192. idx = items.getchildren().index(n)
  193. el = etree.Element('toolbox', attrib={'name': 'dummy'})
  194. items.insert(idx, el)
  195. label = etree.SubElement(el, tag='label')
  196. label.text = _("Toolboxes")
  197. it = etree.SubElement(el, tag='items')
  198. for toolbox in tboxes:
  199. it.append(copy.deepcopy(toolbox))
  200. def _removeUserToolboxesItem(root):
  201. """!Removes tag 'user-toolboxes-list' if there are no user toolboxes."""
  202. for n in root.findall('./items/user-toolboxes-list'):
  203. items = root.find('./items')
  204. items.remove(n)
  205. def _expandItems(node, items, itemTag):
  206. """!Expand items from file"""
  207. for moduleItem in node.findall('.//' + itemTag):
  208. itemName = moduleItem.get('name')
  209. if has_xpath:
  210. moduleNode = items.find('.//%s[@name="%s"]' % (itemTag, itemName))
  211. else:
  212. moduleNode = None
  213. potentialModuleNodes = items.findall('.//%s' % itemTag)
  214. for mNode in potentialModuleNodes:
  215. if mNode.get('name') == itemName:
  216. moduleNode = mNode
  217. break
  218. if moduleNode is None: # module not available in dist
  219. continue
  220. mItemChildren = moduleItem.getchildren()
  221. tagList = [n.tag for n in mItemChildren]
  222. for node in moduleNode.getchildren():
  223. if node.tag not in tagList:
  224. moduleItem.append(node)
  225. def _expandRuntimeModules(node):
  226. """!Add information to modules (desc, keywords)
  227. by running them with --interface-description."""
  228. modules = node.findall('.//module-item')
  229. for module in modules:
  230. name = module.get('name')
  231. if module.find('module') is None:
  232. n = etree.SubElement(parent=module, tag='module')
  233. n.text = name
  234. if module.find('description') is None:
  235. desc, keywords = _loadMetadata(name)
  236. n = etree.SubElement(parent=module, tag='description')
  237. n.text = _escapeXML(desc)
  238. n = etree.SubElement(parent=module, tag='keywords')
  239. n.text = _escapeXML(','.join(keywords))
  240. def _escapeXML(text):
  241. """!Helper function for correct escaping characters for XML.
  242. Duplicate function in core/toolboxes.
  243. """
  244. return text.replace('<', '&lt;').replace("&", '&amp;').replace(">", '&gt;')
  245. def _loadMetadata(module):
  246. """!Load metadata to modules.
  247. @param module module name
  248. @return (description, keywords as a list)
  249. """
  250. try:
  251. task = gtask.parse_interface(module)
  252. except ScriptError:
  253. return '', ''
  254. return task.get_description(full=True), \
  255. task.get_keywords()
  256. def _addHandlers(node):
  257. """!Add missing handlers to modules"""
  258. for n in node.findall('.//module-item'):
  259. if n.find('handler') is None:
  260. handlerNode = etree.SubElement(parent=n, tag='handler')
  261. handlerNode.text = 'OnMenuCmd'
  262. # e.g. g.region -p
  263. for n in node.findall('.//wxgui-item'):
  264. if n.find('command') is not None:
  265. handlerNode = etree.SubElement(parent=n, tag='handler')
  266. handlerNode.text = 'RunMenuCmd'
  267. def _convertTag(node, old, new):
  268. """!Converts tag name."""
  269. for n in node.findall('.//%s' % old):
  270. n.tag = new
  271. def _convertTagAndRemoveAttrib(node, old, new):
  272. "Converts tag name and removes attributes."
  273. for n in node.findall('.//%s' % old):
  274. n.tag = new
  275. n.attrib = {}
  276. def _convertTree(root):
  277. """!Converts tree to be the form readable by core/menutree.py."""
  278. root.attrib = {}
  279. label = root.find('label')
  280. root.remove(label)
  281. _convertTag(root, 'description', 'help')
  282. _convertTag(root, 'wx-id', 'id')
  283. _convertTag(root, 'module', 'command')
  284. _convertTag(root, 'related-module', 'command')
  285. _convertTagAndRemoveAttrib(root, 'wxgui-item', 'menuitem')
  286. _convertTagAndRemoveAttrib(root, 'module-item', 'menuitem')
  287. root.tag = 'menudata'
  288. i1 = root.find('./items')
  289. i1.tag = 'menubar'
  290. _convertTagAndRemoveAttrib(root, 'toolbox', 'menu')
  291. def _getXMLString(root):
  292. """!Adds comment (about aotogenerated file) to XML.
  293. @return XML as string
  294. """
  295. xml = etree.tostring(root, encoding='UTF-8')
  296. return xml.replace("<?xml version='1.0' encoding='UTF-8'?>\n",
  297. "<?xml version='1.0' encoding='UTF-8'?>\n"
  298. "<!--This is an auto-generated file-->\n")
  299. def main():
  300. tree = toolboxes2menudata(userDefined=False)
  301. root = tree.getroot()
  302. sys.stdout.write(_getXMLString(root))
  303. return 0
  304. if __name__ == '__main__':
  305. sys.exit(main())