Ver código fonte

wxGUI/toolboxes: different tree for menu and for search module (missing some formal things)

git-svn-id: https://svn.osgeo.org/grass/grass/trunk@57187 15284696-431f-4ddb-bdfa-cd5b030d7da7
Vaclav Petras 11 anos atrás
pai
commit
a87413aee0

+ 5 - 1
gui/wxpython/Makefile

@@ -1,7 +1,7 @@
 MODULE_TOPDIR = ../..
 MODULE_TOPDIR = ../..
 
 
 SUBDIRS = docs animation mapswipe gmodeler rlisetup psmap dbmgr vdigit iclass
 SUBDIRS = docs animation mapswipe gmodeler rlisetup psmap dbmgr vdigit iclass
-EXTRA_CLEAN_FILES = menustrings.py build_ext.pyc xml/menudata.xml
+EXTRA_CLEAN_FILES = menustrings.py build_ext.pyc xml/menudata.xml xml/module_tree_menudata.xml
 
 
 include $(MODULE_TOPDIR)/include/Make/Dir.make
 include $(MODULE_TOPDIR)/include/Make/Dir.make
 include $(MODULE_TOPDIR)/include/Make/Doxygen.make
 include $(MODULE_TOPDIR)/include/Make/Doxygen.make
@@ -28,6 +28,7 @@ DSTDIRS := $(patsubst %,$(ETCDIR)/%,icons scripts xml)
 default: $(DSTFILES)
 default: $(DSTFILES)
 	-$(MAKE) $(ETCDIR)/xml/module_items.xml
 	-$(MAKE) $(ETCDIR)/xml/module_items.xml
 	-$(MAKE) xml/menudata.xml
 	-$(MAKE) xml/menudata.xml
+	-$(MAKE) xml/module_tree_menudata.xml
 	-$(MAKE) menustrings.py
 	-$(MAKE) menustrings.py
 	$(MAKE) parsubdirs
 	$(MAKE) parsubdirs
 
 
@@ -38,6 +39,9 @@ $(ETCDIR)/%: % | $(PYDSTDIRS) $(DSTDIRS)
 xml/menudata.xml: core/toolboxes.py
 xml/menudata.xml: core/toolboxes.py
 	$(call run_grass,$(PYTHON) $< > $@)
 	$(call run_grass,$(PYTHON) $< > $@)
 
 
+xml/module_tree_menudata.xml: core/toolboxes.py
+	$(call run_grass,$(PYTHON) $< dummy_parameter > $@)
+
 menustrings.py: core/menutree.py $(ETCDIR)/xml/menudata.xml $(ETCDIR)/xml/menudata_modeler.xml $(ETCDIR)/xml/menudata_psmap.xml
 menustrings.py: core/menutree.py $(ETCDIR)/xml/menudata.xml $(ETCDIR)/xml/menudata_modeler.xml $(ETCDIR)/xml/menudata_psmap.xml
 	@echo "# This is a generated file.\n" > $@
 	@echo "# This is a generated file.\n" > $@
 	$(call run_grass,$(PYTHON) $< >> $@)
 	$(call run_grass,$(PYTHON) $< >> $@)

+ 4 - 0
gui/wxpython/core/menutree.py

@@ -49,10 +49,13 @@ if __name__ == '__main__':
 
 
 from core.treemodel import TreeModel, ModuleNode
 from core.treemodel import TreeModel, ModuleNode
 from core.settings import UserSettings
 from core.settings import UserSettings
+from core.toolboxes import expandAddons
 
 
 if not os.getenv("GISBASE"):
 if not os.getenv("GISBASE"):
     sys.exit("GRASS is not running. Exiting...")
     sys.exit("GRASS is not running. Exiting...")
 
 
+
+# TODO: change the system to remove strange derived classes
 class MenuTreeModelBuilder:
 class MenuTreeModelBuilder:
     """!Abstract menu data class"""
     """!Abstract menu data class"""
     def __init__(self, filename):
     def __init__(self, filename):
@@ -62,6 +65,7 @@ class MenuTreeModelBuilder:
                                           subkey = 'selection')
                                           subkey = 'selection')
 
 
         xmlTree = etree.parse(filename)
         xmlTree = etree.parse(filename)
+        expandAddons(xmlTree)
         self.model = TreeModel(ModuleNode)
         self.model = TreeModel(ModuleNode)
         self._createModel(xmlTree)
         self._createModel(xmlTree)
 
 

+ 178 - 20
gui/wxpython/core/toolboxes.py

@@ -43,6 +43,7 @@ from core.gcmd import GError, RunCommand
 import grass.script.task as gtask
 import grass.script.task as gtask
 import grass.script.core as gcore
 import grass.script.core as gcore
 from grass.script.core import ScriptError
 from grass.script.core import ScriptError
+from core.debug import Debug
 
 
 
 
 # this could be placed to functions
 # this could be placed to functions
@@ -67,6 +68,89 @@ def toolboxesOutdated():
         gcore.try_remove(path)
         gcore.try_remove(path)
 
 
 
 
+# TODO: merge the function with getMenuFile
+def getMenudataFile(userRootFile, newFile, fallback):
+    """!Returns path to XML file for building menu.
+
+    Creates toolbox directory where user defined toolboxes should be located.
+    Checks whether it is needed to create new XML file (user changed toolboxes)
+    or the already generated file could be used.
+    If something goes wrong during building or user doesn't modify menu,
+    default file (from distribution) is returned.
+    """
+    Debug.msg(1, "toolboxes.getMenudataFile: {userRootFile}, {newFile}, {fallback}".format(**locals()))
+
+    distributionRootFile = os.path.join(ETCWXDIR, 'xml', userRootFile)
+    userRootFile = os.path.join(GetSettingsPath(), 'toolboxes', userRootFile)
+    if not os.path.exists(userRootFile):
+        userRootFile = None
+
+    ##fallback = os.path.join(ETCWXDIR, 'xml', 'menudata.xml')
+    # always create toolboxes directory if does not exist yet
+    tbDir = _setupToolboxes()
+
+    if tbDir:
+        menudataFile = os.path.join(tbDir, newFile)
+        generateNew = False
+        # when any of main_menu.xml or toolboxes.xml are changed,
+        # generate new menudata.xml
+
+        if os.path.exists(menudataFile):
+            # remove menu file when there is no main_menu and toolboxes
+            if not userToolboxesFile and not userRootFile:
+                os.remove(menudataFile)
+                Debug.msg(2, "toolboxes.getMenudataFile: no user defined files, menudata deleted")
+                return fallback
+
+            if bool(userToolboxesFile) != bool(userRootFile):
+                # always generate new because we don't know if there has been any change
+                generateNew = True
+                Debug.msg(2, "toolboxes.getMenudataFile: only one of the user defined files")
+            else:
+                # if newer files -> generate new
+                menudataTime = os.path.getmtime(menudataFile)
+                if userToolboxesFile:
+                    if os.path.getmtime(userToolboxesFile) > menudataTime:
+                        Debug.msg(2, "toolboxes.getMenudataFile: user toolboxes is newer than menudata")
+                        generateNew = True
+                if userRootFile:
+                    if os.path.getmtime(userRootFile) > menudataTime:
+                        Debug.msg(2, "toolboxes.getMenudataFile: user root file is newer than menudata")
+                        generateNew = True
+        elif userToolboxesFile or userRootFile:
+            Debug.msg(2, "toolboxes.getMenudataFile: no menudata")
+            generateNew = True
+        else:
+            Debug.msg(2, "toolboxes.getMenudataFile: no user defined files")
+            return fallback
+
+        if generateNew:
+            try:
+                # The case when user does not have custom root
+                # file but has toolboxes requieres regeneration.
+                # Unfortunately, this is the case can be often: defined
+                # toolboxes but undefined module tree file.
+                Debug.msg(2, "toolboxes.getMenudataFile: creating a tree")
+                tree = createTree(distributionRootFile=distributionRootFile, userRootFile=userRootFile)
+            except ETREE_EXCEPTIONS:
+                GError(_("Unable to parse user toolboxes XML files. "
+                         "Default toolboxes will be loaded."))
+                return fallback
+
+            try:
+                xml = _getXMLString(tree.getroot())
+                fh = open(menudataFile, 'w')
+                fh.write(xml)
+                fh.close()
+                return menudataFile
+            except:
+                return fallback
+        else:
+            return menudataFile
+    else:
+        return fallback
+
+
 def getMenuFile():
 def getMenuFile():
     """!Returns path to XML file for building menu.
     """!Returns path to XML file for building menu.
 
 
@@ -76,6 +160,7 @@ def getMenuFile():
     If something goes wrong during building or user doesn't modify menu,
     If something goes wrong during building or user doesn't modify menu,
     default file (from distribution) is returned.
     default file (from distribution) is returned.
     """
     """
+    Debug.msg(1, "toolboxes.getMenuFile")
     fallback = os.path.join(ETCWXDIR, 'xml', 'menudata.xml')
     fallback = os.path.join(ETCWXDIR, 'xml', 'menudata.xml')
     # always create toolboxes directory if does not exist yet
     # always create toolboxes directory if does not exist yet
     tbDir = _setupToolboxes()
     tbDir = _setupToolboxes()
@@ -156,6 +241,39 @@ def _createPath(path):
             return False
             return False
     return True
     return True
 
 
+# TODO: merge with toolboxes2menudata
+def createTree(distributionRootFile, userRootFile, userDefined=True):
+    """!Creates XML file with data for menu.
+
+    Parses toolboxes files from distribution and from users,
+    puts them together, adds metadata to modules and convert
+    tree to previous format used for loading menu.
+
+    @param userDefined use toolboxes defined by user or not (during compilation)
+
+    @return ElementTree instance
+    """
+    if userDefined and userRootFile:
+        mainMenu = etree.parse(userRootFile)
+    else:
+        mainMenu = etree.parse(distributionRootFile)
+
+    toolboxes = etree.parse(toolboxesFile)
+
+    if userDefined and userToolboxesFile:
+        userToolboxes = etree.parse(userToolboxesFile)
+    else:
+        userToolboxes = None
+
+    wxguiItems = etree.parse(wxguiItemsFile)
+    moduleItems = etree.parse(moduleItemsFile)
+
+    return toolboxes2menudataInternal(mainMenu=mainMenu,
+                                      toolboxes=toolboxes,
+                                      userToolboxes=userToolboxes,
+                                      wxguiItems=wxguiItems,
+                                      moduleItems=moduleItems)
+
 
 
 def toolboxes2menudata(userDefined=True):
 def toolboxes2menudata(userDefined=True):
     """!Creates XML file with data for menu.
     """!Creates XML file with data for menu.
@@ -190,6 +308,7 @@ def toolboxes2menudata(userDefined=True):
                                       moduleItems=moduleItems)
                                       moduleItems=moduleItems)
 
 
 
 
+# TODO: rename
 def toolboxes2menudataInternal(mainMenu, toolboxes, userToolboxes,
 def toolboxes2menudataInternal(mainMenu, toolboxes, userToolboxes,
                                wxguiItems, moduleItems):
                                wxguiItems, moduleItems):
     """!Creates XML file with data for menu.
     """!Creates XML file with data for menu.
@@ -215,10 +334,10 @@ def toolboxes2menudataInternal(mainMenu, toolboxes, userToolboxes,
     if not userHasToolboxes:
     if not userHasToolboxes:
         _removeUserToolboxesItem(root)
         _removeUserToolboxesItem(root)
 
 
-    _expandAddonsItem(root)
-
     _expandToolboxes(root, toolboxes)
     _expandToolboxes(root, toolboxes)
 
 
+    # we do not expand addons here since they need to be expanded in runtime
+
     _expandItems(root, moduleItems, 'module-item')
     _expandItems(root, moduleItems, 'module-item')
     _expandItems(root, wxguiItems, 'wxgui-item')
     _expandItems(root, wxguiItems, 'wxgui-item')
 
 
@@ -250,6 +369,17 @@ def _indent(elem, level=0):
             elem.tail = i
             elem.tail = i
 
 
 
 
+def expandAddons(tree):
+    """!Expands addons element.
+    """
+    root = tree.getroot()
+    _expandAddonsItem(root)
+    # expanding and converting is done twice, so there is some overhead
+    _expandRuntimeModules(root)
+    _addHandlers(root)
+    _convertTree(root)
+
+
 def _expandToolboxes(node, toolboxes):
 def _expandToolboxes(node, toolboxes):
     """!Expands tree with toolboxes.
     """!Expands tree with toolboxes.
 
 
@@ -344,7 +474,7 @@ def _expandUserToolboxesItem(node, toolboxes):
     >>> toolboxes = etree.fromstring('<toolboxes><toolbox name="UserToolbox"><items><module-item name="g.region"/></items></toolbox></toolboxes>')
     >>> toolboxes = etree.fromstring('<toolboxes><toolbox name="UserToolbox"><items><module-item name="g.region"/></items></toolbox></toolboxes>')
     >>> _expandUserToolboxesItem(tree, toolboxes)
     >>> _expandUserToolboxesItem(tree, toolboxes)
     >>> etree.tostring(tree)
     >>> etree.tostring(tree)
-    '<toolbox><items><toolbox name="GeneratedUserToolboxesList"><label>Toolboxes</label><items><toolbox name="UserToolbox"><items><module-item name="g.region" /></items></toolbox></items></toolbox></items></toolbox>'
+    '<toolbox><items><toolbox name="GeneratedUserToolboxesList"><label>Custom toolboxes</label><items><toolbox name="UserToolbox"><items><module-item name="g.region" /></items></toolbox></items></toolbox></items></toolbox>'
     """
     """
     tboxes = toolboxes.findall('.//toolbox')
     tboxes = toolboxes.findall('.//toolbox')
 
 
@@ -354,7 +484,7 @@ def _expandUserToolboxesItem(node, toolboxes):
         el = etree.Element('toolbox', attrib={'name': 'GeneratedUserToolboxesList'})
         el = etree.Element('toolbox', attrib={'name': 'GeneratedUserToolboxesList'})
         items.insert(idx, el)
         items.insert(idx, el)
         label = etree.SubElement(el, tag='label')
         label = etree.SubElement(el, tag='label')
-        label.text = _("Toolboxes")
+        label.text = _("Custom toolboxes")
         it = etree.SubElement(el, tag='items')
         it = etree.SubElement(el, tag='items')
         for toolbox in tboxes:
         for toolbox in tboxes:
             it.append(copy.deepcopy(toolbox))
             it.append(copy.deepcopy(toolbox))
@@ -374,34 +504,50 @@ def _removeUserToolboxesItem(root):
         items.remove(n)
         items.remove(n)
 
 
 
 
+def _getAddons():
+    return sorted(RunCommand('g.extension', quiet=True, read=True,
+                             flags='a').splitlines())
+
+
+def _removeAddonsItem(node, addonsNodes):
+    # TODO: change impl to be similar with the remove toolboxes
+    for n in addonsNodes:
+        items = node.find('./items')
+        if items is not None:
+            items.remove(n)
+        # because of inconsistent menudata file
+        items = node.find('./menubar')
+        if items is not None:
+            items.remove(n)
+
+
 def _expandAddonsItem(node):
 def _expandAddonsItem(node):
-    """!Expands tag addons (in main_menu.xml) with currently installed addons.append
+    """!Expands addons element with currently installed addons.
 
 
     Note: there is no mechanism yet to tell the gui to rebuild the menudata.xml
     Note: there is no mechanism yet to tell the gui to rebuild the menudata.xml
     file when new addons are added/removed.
     file when new addons are added/removed.
     """
     """
     # no addonsTag -> do nothing
     # no addonsTag -> do nothing
-    addonsTags = node.findall('./items/addons')
+    addonsTags = node.findall('.//addons')
     if not addonsTags:
     if not addonsTags:
         return
         return
     # fetch addons
     # fetch addons
-    addons = sorted(RunCommand('g.extension', quiet=True, read=True,
-                               flags = 'a').splitlines())
+    addons = _getAddons()
 
 
     # no addons -> remove addons tag
     # no addons -> remove addons tag
     if not addons:
     if not addons:
-        for n in addonsTags:    
-            items = node.find('./items')
-            idx = items.getchildren().index(n)
-            items.remove(n)
+        _removeAddonsItem(node, addonsTags)
         return
         return
 
 
     # create addons toolbox
     # create addons toolbox
     # keywords and desc are handled later automatically
     # keywords and desc are handled later automatically
-    for n in addonsTags:    
-        items = node.find('./items')
+    for n in addonsTags:
+        # find parent is not possible with implementation of etree (in 2.7)
+        items = node.find('./menubar')
         idx = items.getchildren().index(n)
         idx = items.getchildren().index(n)
-        el = etree.Element('toolbox', attrib={'name': 'AddonsToolboxesList'})
+        # do not set name since it is already in menudata file
+        # attib={'name': 'AddonsList'}
+        el = etree.Element('menu')
         items.insert(idx, el)
         items.insert(idx, el)
         label = etree.SubElement(el, tag='label')
         label = etree.SubElement(el, tag='label')
         label.text = _("Addons")
         label.text = _("Addons")
@@ -548,7 +694,9 @@ def _convertTree(root):
     """
     """
     root.attrib = {}
     root.attrib = {}
     label = root.find('label')
     label = root.find('label')
-    root.remove(label)
+    # must check because of inconsistent XML menudata file
+    if label is not None:
+        root.remove(label)
     _convertTag(root, 'description', 'help')
     _convertTag(root, 'description', 'help')
     _convertTag(root, 'wx-id', 'id')
     _convertTag(root, 'wx-id', 'id')
     _convertTag(root, 'module', 'command')
     _convertTag(root, 'module', 'command')
@@ -558,7 +706,9 @@ def _convertTree(root):
 
 
     root.tag = 'menudata'
     root.tag = 'menudata'
     i1 = root.find('./items')
     i1 = root.find('./items')
-    i1.tag = 'menubar'
+    # must check because of inconsistent XML menudata file
+    if i1 is not None:
+        i1.tag = 'menubar'
     _convertTagAndRemoveAttrib(root, 'toolbox', 'menu')
     _convertTagAndRemoveAttrib(root, 'toolbox', 'menu')
 
 
 
 
@@ -669,14 +819,22 @@ def main():
 
 
     File is written to the standard output.
     File is written to the standard output.
     """
     """
-    tree = toolboxes2menudata(userDefined=False)
-    root = tree.getroot()
-    sys.stdout.write(_getXMLString(root))
+    # TODO: fix parameter handling
+    if len(sys.argv) > 1:
+        mainFile = os.path.join(ETCWXDIR, 'xml', 'module_tree.xml')
+        tree = createTree(distributionRootFile=mainFile, userRootFile=None, userDefined=False)
+        root = tree.getroot()
+        sys.stdout.write(_getXMLString(root))
+    else:
+        tree = toolboxes2menudata(userDefined=False)
+        root = tree.getroot()
+        sys.stdout.write(_getXMLString(root))
 
 
     return 0
     return 0
 
 
 
 
 if __name__ == '__main__':
 if __name__ == '__main__':
+    # TODO: fix parameter handling
     if len(sys.argv) > 1:
     if len(sys.argv) > 1:
         if sys.argv[1] == 'doctest':
         if sys.argv[1] == 'doctest':
             sys.exit(doc_test())
             sys.exit(doc_test())

+ 6 - 3
gui/wxpython/lmgr/frame.py

@@ -47,7 +47,7 @@ from core.settings         import UserSettings, GetDisplayVectSettings
 from core.utils            import SetAddOnPath, GetLayerNameFromCmd, command2ltype
 from core.utils            import SetAddOnPath, GetLayerNameFromCmd, command2ltype
 from gui_core.preferences  import MapsetAccess, PreferencesDialog
 from gui_core.preferences  import MapsetAccess, PreferencesDialog
 from lmgr.layertree        import LayerTree, LMIcons
 from lmgr.layertree        import LayerTree, LMIcons
-from lmgr.menudata         import LayerManagerMenuData
+from lmgr.menudata         import LayerManagerMenuData, LayerManagerModuleTree
 from gui_core.widgets      import GNotebook
 from gui_core.widgets      import GNotebook
 from modules.mcalc_builder import MapCalcFrame
 from modules.mcalc_builder import MapCalcFrame
 from dbmgr.manager         import AttributeManager
 from dbmgr.manager         import AttributeManager
@@ -105,7 +105,10 @@ class GMFrame(wx.Frame):
         
         
         self._giface = LayerManagerGrassInterface(self)
         self._giface = LayerManagerGrassInterface(self)
         
         
+        # the main menu bar
         self._menuTreeBuilder = LayerManagerMenuData()
         self._menuTreeBuilder = LayerManagerMenuData()
+        # the search tree and command console
+        self._moduleTreeBuilder = LayerManagerModuleTree()
         self._auimgr = wx.aui.AuiManager(self)
         self._auimgr = wx.aui.AuiManager(self)
         
         
         
         
@@ -279,7 +282,7 @@ class GMFrame(wx.Frame):
         self._gconsole = GConsole(guiparent = self, giface = self._giface,
         self._gconsole = GConsole(guiparent = self, giface = self._giface,
                                   ignoredCmdPattern = '^d\..*|^r[3]?\.mapcalc$|^i.group')
                                   ignoredCmdPattern = '^d\..*|^r[3]?\.mapcalc$|^i.group')
         self.goutput = GConsoleWindow(parent = self, gconsole = self._gconsole,
         self.goutput = GConsoleWindow(parent = self, gconsole = self._gconsole,
-                                      menuModel=self._menuTreeBuilder.GetModel(),
+                                      menuModel=self._moduleTreeBuilder.GetModel(),
                                       gcstyle = GC_SEARCH | GC_PROMPT)
                                       gcstyle = GC_SEARCH | GC_PROMPT)
         self.notebook.AddPage(page = self.goutput, text = _("Command console"), name = 'output')
         self.notebook.AddPage(page = self.goutput, text = _("Command console"), name = 'output')
 
 
@@ -317,7 +320,7 @@ class GMFrame(wx.Frame):
         
         
         # create 'search module' notebook page
         # create 'search module' notebook page
         if not UserSettings.Get(group = 'manager', key = 'hideTabs', subkey = 'search'):
         if not UserSettings.Get(group = 'manager', key = 'hideTabs', subkey = 'search'):
-            self.search = SearchModuleWindow(parent = self, model=self._menuTreeBuilder.GetModel())
+            self.search = SearchModuleWindow(parent = self, model=self._moduleTreeBuilder.GetModel())
             self.search.showNotification.connect(lambda message: self.SetStatusText(message))
             self.search.showNotification.connect(lambda message: self.SetStatusText(message))
             self.notebook.AddPage(page = self.search, text = _("Search modules"), name = 'search')
             self.notebook.AddPage(page = self.search, text = _("Search modules"), name = 'search')
         else:
         else:

+ 17 - 1
gui/wxpython/lmgr/menudata.py

@@ -18,7 +18,7 @@ This program is free software under the GNU General Public License
 import os
 import os
 
 
 from core.menutree  import MenuTreeModelBuilder
 from core.menutree  import MenuTreeModelBuilder
-from core.toolboxes import getMenuFile
+from core.toolboxes import getMenuFile, getMenudataFile
 from core.globalvar import ETCWXDIR
 from core.globalvar import ETCWXDIR
 from core.gcmd import GError
 from core.gcmd import GError
 
 
@@ -34,3 +34,19 @@ class LayerManagerMenuData(MenuTreeModelBuilder):
                      "Default toolboxes will be loaded."))
                      "Default toolboxes will be loaded."))
             fallback = os.path.join(ETCWXDIR, 'xml', 'menudata.xml')
             fallback = os.path.join(ETCWXDIR, 'xml', 'menudata.xml')
             MenuTreeModelBuilder.__init__(self, fallback)
             MenuTreeModelBuilder.__init__(self, fallback)
+
+
+class LayerManagerModuleTree(MenuTreeModelBuilder):
+    def __init__(self, filename=None):
+        fallback = os.path.join(ETCWXDIR, 'xml', 'module_tree_menudata.xml')
+        if not filename:
+            filename = getMenudataFile(userRootFile='module_tree.xml',
+                                       newFile='module_tree_menudata.xml',
+                                       fallback=fallback)
+        # TODO: try-except useless?
+        try:
+            MenuTreeModelBuilder.__init__(self, filename)
+        except (ValueError, AttributeError, TypeError):
+            GError(_("Unable to parse user toolboxes XML files. "
+                     "Default toolboxes will be loaded."))
+            MenuTreeModelBuilder.__init__(self, fallback)

+ 0 - 1
gui/wxpython/xml/main_menu.xml

@@ -10,7 +10,6 @@
     <subtoolbox name="Imagery"/>
     <subtoolbox name="Imagery"/>
     <subtoolbox name="Volumes"/>
     <subtoolbox name="Volumes"/>
     <subtoolbox name="Database"/>
     <subtoolbox name="Database"/>
-    <user-toolboxes-list />
     <subtoolbox name="Help"/>
     <subtoolbox name="Help"/>
   </items>
   </items>
 </toolbox>
 </toolbox>

+ 16 - 0
gui/wxpython/xml/module_tree.xml

@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE toolbox SYSTEM "main_menu.dtd">
+<toolbox name="DefaultModuleTree">
+  <label>Default GRASS GIS module tree</label>
+  <items>
+    <subtoolbox name="Raster"/>
+    <subtoolbox name="Vector"/>
+    <subtoolbox name="Imagery"/>
+    <subtoolbox name="Volumes"/>
+    <subtoolbox name="Database"/>
+    <subtoolbox name="Temporal"/>
+    <user-toolboxes-list/>
+    <addons/>
+  </items>
+</toolbox>
+