Преглед изворни кода

Reflect grassdb changes in catalog (#994)

Introduces new signal to update catalog tree when a change in grass database happened.
So far it covers cases when a new map is created through running a module (from command line or dialog), imports, mapcalc.

Also updates tree when new mapset/location is created from menu. Here it changes behavior, it doesn't automatically switch to the new mapset.

Additionally it uses python watchdog library to observe current mapset to reflect changes from terminal or other sources not covered by mechanism above. Only current mapset is observed because there is a low default limit on how many files can be watched. Watchdog is not part of standard Python lib but can be installed through pip.
Anna Petrasova пре 4 година
родитељ
комит
32bb3de1aa

+ 7 - 0
gui/wxpython/core/gconsole.py

@@ -737,6 +737,13 @@ class GConsole(wx.EvtHandler):
                                 'fullname']:
                                 'fullname']:
                             self.mapCreated.emit(
                             self.mapCreated.emit(
                                 name=lname, ltype=prompt, add=event.addLayer)
                                 name=lname, ltype=prompt, add=event.addLayer)
+                            gisenv = grass.gisenv()
+                            self._giface.grassdbChanged.emit(grassdb=gisenv['GISDBASE'],
+                                                             location=gisenv['LOCATION_NAME'],
+                                                             mapset=gisenv['MAPSET'],
+                                                             action='new',
+                                                             map=lname.split('@')[0],
+                                                             element=prompt)
         if name == 'r.mask':
         if name == 'r.mask':
             self.updateMap.emit()
             self.updateMap.emit()
 
 

+ 14 - 1
gui/wxpython/core/giface.py

@@ -217,11 +217,24 @@ class StandaloneGrassInterface(GrassInterface):
     def __init__(self):
     def __init__(self):
 
 
         # Signal when some map is created or updated by a module.
         # Signal when some map is created or updated by a module.
+        # Used for adding/refreshing displayed layers.
         # attributes: name: map name, ltype: map type,
         # attributes: name: map name, ltype: map type,
         # add: if map should be added to layer tree (questionable attribute)
         # add: if map should be added to layer tree (questionable attribute)
         self.mapCreated = Signal('StandaloneGrassInterface.mapCreated')
         self.mapCreated = Signal('StandaloneGrassInterface.mapCreated')
 
 
-        self.currentMapsetChanged = Signal('LayerManagerGrassInterface.currentMapsetChanged')
+        # Signal for communicating current mapset has been switched
+        self.currentMapsetChanged = Signal('StandaloneGrassInterface.currentMapsetChanged')
+
+        # Signal for communicating something in current grassdb has changed.
+        # Parameters:
+        # action: required, is one of 'new', 'rename', 'delete'
+        # element: required, can be one of 'grassdb', 'location', 'mapset', 'raster', 'vector' and 'raster_3d'
+        # grassdb: path to grass db, required
+        # location: location name, required
+        # mapset: mapset name, required when element is 'mapset', 'raster', 'vector' or 'raster_3d'
+        # map: map name, required when element is 'raster', 'vector' or 'raster_3d'
+        # newname: new name (of mapset, map), required with action='rename'
+        self.grassdbChanged = Signal('StandaloneGrassInterface.grassdbChanged')
 
 
         # Signal emitted to request updating of map
         # Signal emitted to request updating of map
         self.updateMap = Signal('StandaloneGrassInterface.updateMap')
         self.updateMap = Signal('StandaloneGrassInterface.updateMap')

+ 281 - 42
gui/wxpython/datacatalog/tree.py

@@ -23,7 +23,16 @@ import re
 import copy
 import copy
 from multiprocessing import Process, Queue, cpu_count
 from multiprocessing import Process, Queue, cpu_count
 
 
+watchdog_used = True
+try:
+    from watchdog.observers import Observer
+    from watchdog.events import PatternMatchingEventHandler
+except ImportError:
+    watchdog_used = False
+
+
 import wx
 import wx
+from wx.lib.newevent import NewEvent
 
 
 from core.gcmd import RunCommand, GError, GMessage, GWarning
 from core.gcmd import RunCommand, GError, GMessage, GWarning
 from core.utils import GetListOfLocations
 from core.utils import GetListOfLocations
@@ -68,6 +77,9 @@ from grass.grassdb.checks import (get_mapset_owner, is_mapset_locked,
 from grass.exceptions import CalledModuleError
 from grass.exceptions import CalledModuleError
 
 
 
 
+updateMapset, EVT_UPDATE_MAPSET = NewEvent()
+
+
 def filterModel(model, element=None, name=None):
 def filterModel(model, element=None, name=None):
     """Filter tree model based on type or name of map using regular expressions.
     """Filter tree model based on type or name of map using regular expressions.
     Copies tree and remove nodes which don't match."""
     Copies tree and remove nodes which don't match."""
@@ -174,6 +186,41 @@ def getLocationTree(gisdbase, location, queue, mapsets=None):
     gscript.try_remove(tmp_gisrc_file)
     gscript.try_remove(tmp_gisrc_file)
 
 
 
 
+class MapWatch(PatternMatchingEventHandler):
+    """Monitors file events (create, delete, move files) using watchdog
+    to inform about changes in current mapset. One instance monitors
+    only one element (raster, vector, raster_3d).
+    Patterns are not used/needed in this case, use just '*' for matching
+    everything. When file/directory change is detected, wx event is dispatched
+    to event handler (can't use Signals because this is different thread),
+    containing info about the change."""
+    def __init__(self, patterns, element, event_handler):
+        PatternMatchingEventHandler.__init__(self, patterns=patterns)
+        self.element = element
+        self.event_handler = event_handler
+        
+    def on_created(self, event):
+        if (self.element == 'vector' or self.element == 'raster_3d') and not event.is_directory:
+            return
+        evt = updateMapset(src_path=event.src_path, event_type=event.event_type,
+                           is_directory=event.is_directory, dest_path=None)
+        wx.PostEvent(self.event_handler, evt)
+
+    def on_deleted(self, event):
+        if (self.element == 'vector' or self.element == 'raster_3d') and not event.is_directory:
+            return
+        evt = updateMapset(src_path=event.src_path, event_type=event.event_type,
+                           is_directory=event.is_directory, dest_path=None)
+        wx.PostEvent(self.event_handler, evt)
+
+    def on_moved(self, event):
+        if (self.element == 'vector' or self.element == 'raster_3d') and not event.is_directory:
+            return
+        evt = updateMapset(src_path=event.src_path, event_type=event.event_type,
+                           is_directory=event.is_directory, dest_path=event.dest_path)
+        wx.PostEvent(self.event_handler, evt)  
+
+
 class NameEntryDialog(TextEntryDialog):
 class NameEntryDialog(TextEntryDialog):
 
 
     def __init__(self, element, mapset, env, **kwargs):
     def __init__(self, element, mapset, env, **kwargs):
@@ -251,7 +298,15 @@ class DataCatalogNode(DictNode):
 
 
 
 
 class DataCatalogTree(TreeView):
 class DataCatalogTree(TreeView):
-
+    """Tree structure visualizing and managing grass database.
+    Uses virtual tree and model defined in core/treemodel.py.
+
+    When changes to data are initiated from inside, the model
+    and the tree are not changed directly, rather a grassdbChanged
+    signal needs to be emitted and the handler of the signal
+    takes care of the refresh. At the same time, watchdog (if installed)
+    monitors changes in current mapset and refreshes the tree.
+    """
     def __init__(
     def __init__(
             self, parent, model=None, giface=None,
             self, parent, model=None, giface=None,
             style=wx.TR_HIDE_ROOT | wx.TR_EDIT_LABELS |
             style=wx.TR_HIDE_ROOT | wx.TR_EDIT_LABELS |
@@ -276,6 +331,7 @@ class DataCatalogTree(TreeView):
         self.contextMenu.connect(self.OnRightClick)
         self.contextMenu.connect(self.OnRightClick)
         self.itemActivated.connect(self.OnDoubleClick)
         self.itemActivated.connect(self.OnDoubleClick)
         self._giface.currentMapsetChanged.connect(self._updateAfterMapsetChanged)
         self._giface.currentMapsetChanged.connect(self._updateAfterMapsetChanged)
+        self._giface.grassdbChanged.connect(self._updateAfterGrassdbChanged)
         self._iconTypes = ['grassdb', 'location', 'mapset', 'raster',
         self._iconTypes = ['grassdb', 'location', 'mapset', 'raster',
                            'vector', 'raster_3d']
                            'vector', 'raster_3d']
         self._initImages()
         self._initImages()
@@ -314,6 +370,9 @@ class DataCatalogTree(TreeView):
                   self._emitSignal(evt.GetItem(), self.endEdit, event=evt))
                   self._emitSignal(evt.GetItem(), self.endEdit, event=evt))
         self.startEdit.connect(self.OnStartEditLabel)
         self.startEdit.connect(self.OnStartEditLabel)
         self.endEdit.connect(self.OnEditLabel)
         self.endEdit.connect(self.OnEditLabel)
+        self.Bind(EVT_UPDATE_MAPSET, self.OnWatchdogMapsetReload)
+        
+        self.observer = None
 
 
     def _resetSelectVariables(self):
     def _resetSelectVariables(self):
         """Reset variables related to item selection."""
         """Reset variables related to item selection."""
@@ -511,6 +570,73 @@ class DataCatalogTree(TreeView):
             return errors
             return errors
         return None
         return None
 
 
+    def ScheduleWatchCurrentMapset(self):
+        """Using watchdog library, sets up watching of current mapset folder
+        to detect changes not captured by other means (e.g. from command line).
+        Schedules 3 watches (raster, vector, 3D raster).
+        If watchdog observers are active, it restarts the observers in current mapset.
+        """
+        global watchdog_used
+        if not watchdog_used:
+            return
+
+        if self.observer and self.observer.is_alive():
+            self.observer.stop()
+            self.observer.join()
+            self.observer.unschedule_all()
+        self.observer = Observer()
+
+        gisenv = gscript.gisenv()
+        for element, directory in (('raster', 'cell'), ('vector', 'vector'), ('raster_3d', 'grid3')):
+            path = os.path.join(gisenv['GISDBASE'], gisenv['LOCATION_NAME'],
+                                gisenv['MAPSET'], directory)
+            if not os.path.exists(path):
+                try:
+                    os.mkdir(path)
+                except OSError:
+                    pass
+            if os.path.exists(path):
+                self.observer.schedule(MapWatch("*", element, self), path=path, recursive=False)
+        try:
+            self.observer.start()
+        except OSError:
+            # in case inotify on linux exceeds limits
+            watchdog_used = False
+            return
+
+    def OnWatchdogMapsetReload(self, event):
+        """Reload mapset node associated with watchdog event"""
+        mapset_path = os.path.dirname(os.path.dirname(os.path.abspath(event.src_path)))
+        location_path = os.path.dirname(os.path.abspath(mapset_path))
+        db = os.path.dirname(os.path.abspath(location_path))
+        node = self.GetDbNode(grassdb=db, location=os.path.basename(location_path),
+                              mapset=os.path.basename(mapset_path))
+        if node:
+            self._reloadMapsetNode(node)
+            self.RefreshNode(node, recursive=True)
+
+    def GetDbNode(self, grassdb, location=None, mapset=None, map=None, map_type=None):
+        """Returns node representing db/location/mapset/map or None if not found."""
+        grassdb_nodes = self._model.SearchNodes(name=grassdb, type='grassdb')
+        if grassdb_nodes:
+            if not location:
+                return grassdb_nodes[0]
+            location_nodes = self._model.SearchNodes(parent=grassdb_nodes[0],
+                                                     name=location, type='location')
+            if location_nodes:
+                if not mapset:
+                    return location_nodes[0]
+                mapset_nodes = self._model.SearchNodes(parent=location_nodes[0],
+                                                       name=mapset, type='mapset')
+                if mapset_nodes:
+                    if not map:
+                        return mapset_nodes[0]
+                    map_nodes = self._model.SearchNodes(parent=mapset_nodes[0],
+                                                        name=map, type=map_type)
+                    if map_nodes:
+                        return map_nodes[0]
+        return None
+
     def _renameNode(self, node, name):
     def _renameNode(self, node, name):
         """Rename node (map, mapset, location), sort and refresh.
         """Rename node (map, mapset, location), sort and refresh.
         Should be called after actual renaming of a map, mapset, location."""
         Should be called after actual renaming of a map, mapset, location."""
@@ -571,6 +697,7 @@ class DataCatalogTree(TreeView):
         if event.ret is not None:
         if event.ret is not None:
             self._giface.WriteWarning('\n'.join(event.ret))
             self._giface.WriteWarning('\n'.join(event.ret))
         self.UpdateCurrentDbLocationMapsetNode()
         self.UpdateCurrentDbLocationMapsetNode()
+        self.ScheduleWatchCurrentMapset()
         self.RefreshItems()
         self.RefreshItems()
         self.ExpandCurrentMapset()
         self.ExpandCurrentMapset()
 
 
@@ -844,8 +971,11 @@ class DataCatalogTree(TreeView):
         mapset = create_mapset_interactively(self, grassdb_node.data['name'],
         mapset = create_mapset_interactively(self, grassdb_node.data['name'],
                                              location_node.data['name'])
                                              location_node.data['name'])
         if mapset:
         if mapset:
-            self.InsertMapset(name=mapset,
-                              location_node=location_node)
+            self._giface.grassdbChanged.emit(grassdb=grassdb_node.data['name'],
+                                             location=location_node.data['name'],
+                                             mapset=mapset,
+                                             element='mapset',
+                                             action='new')
 
 
     def OnCreateMapset(self, event):
     def OnCreateMapset(self, event):
         """Create new mapset"""
         """Create new mapset"""
@@ -859,10 +989,10 @@ class DataCatalogTree(TreeView):
             create_location_interactively(self, grassdb_node.data['name'])
             create_location_interactively(self, grassdb_node.data['name'])
         )
         )
         if location:
         if location:
-            grassdb_nodes = self._model.SearchNodes(name=grassdatabase, type='grassdb')
-            if not grassdb_nodes:
-                grassdb_node = self.InsertGrassDb(name=grassdatabase)
-            self.InsertLocation(location, grassdb_node)
+            self._giface.grassdbChanged.emit(grassdb=grassdatabase,
+                                             location=location,
+                                             element='location',
+                                             action='new')
 
 
     def OnCreateLocation(self, event):
     def OnCreateLocation(self, event):
         """Create new location"""
         """Create new location"""
@@ -878,7 +1008,12 @@ class DataCatalogTree(TreeView):
                 self.selected_location[0].data['name'],
                 self.selected_location[0].data['name'],
                 self.selected_mapset[0].data['name'])
                 self.selected_mapset[0].data['name'])
         if newmapset:
         if newmapset:
-            self._renameNode(self.selected_mapset[0], newmapset)
+            self._giface.grassdbChanged.emit(grassdb=self.selected_grassdb[0].data['name'],
+                                             location=self.selected_location[0].data['name'],
+                                             mapset=self.selected_mapset[0].data['name'],
+                                             element='mapset',
+                                             action='rename',
+                                             newname=newmapset)
 
 
     def OnRenameLocation(self, event):
     def OnRenameLocation(self, event):
         """
         """
@@ -889,7 +1024,11 @@ class DataCatalogTree(TreeView):
                 self.selected_grassdb[0].data['name'],
                 self.selected_grassdb[0].data['name'],
                 self.selected_location[0].data['name'])
                 self.selected_location[0].data['name'])
         if newlocation:
         if newlocation:
-            self._renameNode(self.selected_location[0], newlocation)
+            self._giface.grassdbChanged.emit(grassdb=self.selected_grassdb[0].data['name'],
+                                             location=self.selected_location[0].data['name'],
+                                             element='location',
+                                             action='rename',
+                                             newname=newlocation)
 
 
     def OnStartEditLabel(self, node, event):
     def OnStartEditLabel(self, node, event):
         """Start label editing"""
         """Start label editing"""
@@ -989,12 +1128,18 @@ class DataCatalogTree(TreeView):
         else:
         else:
             renamed, cmd = self._runCommand(
             renamed, cmd = self._runCommand(
                 'g.rename', raster3d=string, env=env)
                 'g.rename', raster3d=string, env=env)
+        gscript.try_remove(gisrc)
         if renamed == 0:
         if renamed == 0:
-            self._renameNode(self.selected_layer[0], new)
             self.showNotification.emit(
             self.showNotification.emit(
                 message=_("{cmd} -- completed").format(cmd=cmd))
                 message=_("{cmd} -- completed").format(cmd=cmd))
             Debug.msg(1, "LAYER RENAMED TO: " + new)
             Debug.msg(1, "LAYER RENAMED TO: " + new)
-        gscript.try_remove(gisrc)
+            self._giface.grassdbChanged.emit(grassdb=self.selected_grassdb[0].data['name'],
+                                             location=self.selected_location[0].data['name'],
+                                             mapset=self.selected_mapset[0].data['name'],
+                                             map=old,
+                                             element=self.selected_layer[0].data['type'],
+                                             newname=new,
+                                             action='rename')
 
 
     def OnPasteMap(self, event):
     def OnPasteMap(self, event):
         # copying between mapsets of one location
         # copying between mapsets of one location
@@ -1055,14 +1200,17 @@ class DataCatalogTree(TreeView):
                     pasted, cmd = self._runCommand('g.copy', raster_3d=string, env=env)
                     pasted, cmd = self._runCommand('g.copy', raster_3d=string, env=env)
                     node = 'raster_3d'
                     node = 'raster_3d'
                 if pasted == 0:
                 if pasted == 0:
-                    self.InsertLayer(name=new_name, mapset_node=self.selected_mapset[0],
-                                     element_name=node)
                     Debug.msg(1, "COPIED TO: " + new_name)
                     Debug.msg(1, "COPIED TO: " + new_name)
                     if self.copy_mode:
                     if self.copy_mode:
                         self.showNotification.emit(message=_("g.copy completed"))
                         self.showNotification.emit(message=_("g.copy completed"))
                     else:
                     else:
                         self.showNotification.emit(message=_("g.copy completed"))
                         self.showNotification.emit(message=_("g.copy completed"))
-
+                    self._giface.grassdbChanged.emit(grassdb=self.selected_grassdb[0].data['name'],
+                                                     location=self.selected_location[0].data['name'],
+                                                     mapset=self.selected_mapset[0].data['name'],
+                                                     map=new_name,
+                                                     element=node,
+                                                     action='new')
                     # remove old
                     # remove old
                     if not self.copy_mode:
                     if not self.copy_mode:
                         self._removeMapAfterCopy(self.copy_layer[i], self.copy_mapset[i], env2)
                         self._removeMapAfterCopy(self.copy_layer[i], self.copy_mapset[i], env2)
@@ -1083,8 +1231,9 @@ class DataCatalogTree(TreeView):
                     if not new_name:
                     if not new_name:
                         continue
                         continue
                 callback = lambda gisrc2=gisrc2, gisrc=gisrc, cLayer=self.copy_layer[i], \
                 callback = lambda gisrc2=gisrc2, gisrc=gisrc, cLayer=self.copy_layer[i], \
-                                  cMapset=self.copy_mapset[i], cMode=self.copy_mode, name=new_name: \
-                                  self._onDoneReprojection(env2, gisrc2, gisrc, cLayer, cMapset, cMode, name)
+                                  cMapset=self.copy_mapset[i], cMode=self.copy_mode, \
+                                  sMapset=self.selected_mapset[0], name=new_name: \
+                                  self._onDoneReprojection(env2, gisrc2, gisrc, cLayer, cMapset, cMode, sMapset, name)
                 dlg = CatalogReprojectionDialog(self, self._giface,
                 dlg = CatalogReprojectionDialog(self, self._giface,
                                                 self.copy_grassdb[i].data['name'],
                                                 self.copy_grassdb[i].data['name'],
                                                 self.copy_location[i].data['name'],
                                                 self.copy_location[i].data['name'],
@@ -1102,23 +1251,31 @@ class DataCatalogTree(TreeView):
         self.ExpandNode(self.selected_mapset[0], recursive=True)
         self.ExpandNode(self.selected_mapset[0], recursive=True)
         self._resetCopyVariables()
         self._resetCopyVariables()
 
 
-    def _onDoneReprojection(self, iEnv, iGisrc, oGisrc, cLayer, cMapset, cMode, name):
-        self.InsertLayer(name=name, mapset_node=self.selected_mapset[0],
-                         element_name=cLayer.data['type'])
+    def _onDoneReprojection(self, iEnv, iGisrc, oGisrc, cLayer, cMapset, cMode, sMapset, name):
+        self._giface.grassdbChanged.emit(grassdb=sMapset.parent.parent.data['name'],
+                                         location=sMapset.parent.data['name'],
+                                         mapset=sMapset.data['name'],
+                                         element=cLayer.data['type'],
+                                         map=name,
+                                         action='new')
         if not cMode:
         if not cMode:
             self._removeMapAfterCopy(cLayer, cMapset, iEnv)
             self._removeMapAfterCopy(cLayer, cMapset, iEnv)
         gscript.try_remove(iGisrc)
         gscript.try_remove(iGisrc)
         gscript.try_remove(oGisrc)
         gscript.try_remove(oGisrc)
-        self.ExpandNode(self.selected_mapset[0], recursive=True)
+        self.ExpandNode(sMapset, recursive=True)
 
 
     def _removeMapAfterCopy(self, cLayer, cMapset, env):
     def _removeMapAfterCopy(self, cLayer, cMapset, env):
         removed, cmd = self._runCommand('g.remove', type=cLayer.data['type'],
         removed, cmd = self._runCommand('g.remove', type=cLayer.data['type'],
                                         name=cLayer.data['name'], flags='f', env=env)
                                         name=cLayer.data['name'], flags='f', env=env)
         if removed == 0:
         if removed == 0:
-            self._model.RemoveNode(cLayer)
-            self.RefreshNode(cMapset, recursive=True)
             Debug.msg(1, "LAYER " + cLayer.data['name'] + " DELETED")
             Debug.msg(1, "LAYER " + cLayer.data['name'] + " DELETED")
             self.showNotification.emit(message=_("g.remove completed"))
             self.showNotification.emit(message=_("g.remove completed"))
+            self._giface.grassdbChanged.emit(grassdb=cMapset.parent.parent.data['name'],
+                                             location=cMapset.parent.data['name'],
+                                             mapset=cMapset.data['name'],
+                                             map=cLayer.data['name'],
+                                             element=cLayer.data['type'],
+                                             action='delete')
 
 
     def InsertLayer(self, name, mapset_node, element_name):
     def InsertLayer(self, name, mapset_node, element_name):
         """Insert layer into model and refresh tree"""
         """Insert layer into model and refresh tree"""
@@ -1191,19 +1348,16 @@ class DataCatalogTree(TreeView):
                 removed, cmd = self._runCommand(
                 removed, cmd = self._runCommand(
                         'g.remove', flags='f', type=self.selected_layer[i].data['type'],
                         'g.remove', flags='f', type=self.selected_layer[i].data['type'],
                         name=self.selected_layer[i].data['name'], env=env)
                         name=self.selected_layer[i].data['name'], env=env)
+                gscript.try_remove(gisrc)
                 if removed == 0:
                 if removed == 0:
-                    self._model.RemoveNode(self.selected_layer[i])
-                    self.RefreshNode(self.selected_mapset[i], recursive=True)
+                    self._giface.grassdbChanged.emit(grassdb=self.selected_grassdb[i].data['name'],
+                                                     location=self.selected_location[i].data['name'],
+                                                     mapset=self.selected_mapset[i].data['name'],
+                                                     element=self.selected_layer[i].data['type'],
+                                                     map=self.selected_layer[i].data['name'],
+                                                     action='delete')
                     Debug.msg(1, "LAYER " + self.selected_layer[i].data['name'] + " DELETED")
                     Debug.msg(1, "LAYER " + self.selected_layer[i].data['name'] + " DELETED")
 
 
-                    # remove map layer from layer tree if exists
-                    if not isinstance(self._giface, StandaloneGrassInterface):
-                        name = self.selected_layer[i].data['name'] + '@' + self.selected_mapset[i].data['name']
-                        layers = self._giface.GetLayerList().GetLayersByName(name)
-                        for layer in layers:
-                            self._giface.GetLayerList().DeleteLayer(layer)
-
-                gscript.try_remove(gisrc)
             self.UnselectAll()
             self.UnselectAll()
             self.showNotification.emit(message=_("g.remove completed"))
             self.showNotification.emit(message=_("g.remove completed"))
 
 
@@ -1212,6 +1366,7 @@ class DataCatalogTree(TreeView):
         Delete selected mapset or mapsets
         Delete selected mapset or mapsets
         """
         """
         mapsets = []
         mapsets = []
+        changes = []
         for i in range(len(self.selected_mapset)):
         for i in range(len(self.selected_mapset)):
             # Append to the list of tuples
             # Append to the list of tuples
             mapsets.append((
             mapsets.append((
@@ -1219,30 +1374,34 @@ class DataCatalogTree(TreeView):
                 self.selected_location[i].data['name'],
                 self.selected_location[i].data['name'],
                 self.selected_mapset[i].data['name']
                 self.selected_mapset[i].data['name']
             ))
             ))
+            changes.append(dict(grassdb=self.selected_grassdb[i].data['name'],
+                                location=self.selected_location[i].data['name'],
+                                mapset=self.selected_mapset[i].data['name'],
+                                action='delete',
+                                element='mapset'))
         if delete_mapsets_interactively(self, mapsets):
         if delete_mapsets_interactively(self, mapsets):
-            locations = set([each for each in self.selected_location])
-            for loc_node in locations:
-                self._reloadLocationNode(loc_node)
-                self.UpdateCurrentDbLocationMapsetNode()
-                self.RefreshNode(loc_node, recursive=True)
+            for change in changes:
+                self._giface.grassdbChanged.emit(**change)
 
 
     def OnDeleteLocation(self, event):
     def OnDeleteLocation(self, event):
         """
         """
         Delete selected location or locations
         Delete selected location or locations
         """
         """
         locations = []
         locations = []
+        changes = []
         for i in range(len(self.selected_location)):
         for i in range(len(self.selected_location)):
             # Append to the list of tuples
             # Append to the list of tuples
             locations.append((
             locations.append((
                 self.selected_grassdb[i].data['name'],
                 self.selected_grassdb[i].data['name'],
                 self.selected_location[i].data['name']
                 self.selected_location[i].data['name']
             ))
             ))
+            changes.append(dict(grassdb=self.selected_grassdb[i].data['name'],
+                                location=self.selected_location[i].data['name'],
+                                action='delete',
+                                element='location'))
         if delete_locations_interactively(self, locations):
         if delete_locations_interactively(self, locations):
-            grassdbs = set([each for each in self.selected_grassdb])
-            for grassdb_node in grassdbs:
-                self._reloadGrassDBNode(grassdb_node)
-                self.UpdateCurrentDbLocationMapsetNode()
-                self.RefreshNode(grassdb_node, recursive=True)
+            for change in changes:
+                self._giface.grassdbChanged.emit(**change)
 
 
     def DownloadLocation(self, grassdb_node):
     def DownloadLocation(self, grassdb_node):
         """
         """
@@ -1383,11 +1542,91 @@ class DataCatalogTree(TreeView):
             else:
             else:
                 switch_mapset_interactively(self, self._giface, grassdb, location, mapset)
                 switch_mapset_interactively(self, self._giface, grassdb, location, mapset)
 
 
+    def _updateAfterGrassdbChanged(self, action, element, grassdb, location, mapset=None,
+                                   map=None, newname=None):
+        """Update tree after grassdata changed"""
+        if element == 'mapset':
+            if action == 'new':
+                node = self.GetDbNode(grassdb=grassdb,
+                                      location=location)
+                if node:
+                    self.InsertMapset(name=mapset, location_node=node)
+            elif action == 'delete':
+                node = self.GetDbNode(grassdb=grassdb,
+                                      location=location)
+                if node:
+                    self._reloadLocationNode(node)
+                    self.UpdateCurrentDbLocationMapsetNode()
+                    self.RefreshNode(node, recursive=True)
+            elif action == 'rename':
+                node = self.GetDbNode(grassdb=grassdb,
+                                      location=location,
+                                      mapset=mapset)
+                if node:
+                    self._renameNode(node, newname)
+        elif element == 'location':
+            if action == 'new':
+                node = self.GetDbNode(grassdb=grassdb)
+                if not node:
+                    node = self.InsertGrassDb(name=grassdb)
+                if node:
+                    self.InsertLocation(location, node)
+            elif action == 'delete':
+                node = self.GetDbNode(grassdb=grassdb)
+                if node:
+                    self._reloadGrassDBNode(node)
+                    self.RefreshNode(node, recursive=True)
+            elif action == 'rename':
+                node = self.GetDbNode(grassdb=grassdb,
+                                      location=location)
+                if node:
+                    self._renameNode(node, newname)
+        elif element in ('raster', 'vector', 'raster_3d'):
+            # when watchdog is used, it watches current mapset,
+            # so we don't process any signals here,
+            # instead the watchdog handler takes care of refreshing tree
+            if (watchdog_used and grassdb == self.current_grassdb_node.data['name']
+               and location == self.current_location_node.data['name']
+               and mapset == self.current_mapset_node.data['name']):
+                return
+            if action == 'new':
+                node = self.GetDbNode(grassdb=grassdb,
+                                      location=location,
+                                      mapset=mapset)
+                if node:
+                    if map:
+                        # check if map already exists
+                        if not self._model.SearchNodes(parent=node, name=newname, type=element):
+                            self.InsertLayer(name=newname, mapset_node=node,
+                                             element_name=element)
+                    else:
+                        # we know some maps created
+                        self._reloadMapsetNode(node)
+                        self.RefreshNode(node)
+            elif action == 'delete':
+                node = self.GetDbNode(grassdb=grassdb,
+                                      location=location,
+                                      mapset=mapset,
+                                      map=map,
+                                      map_type=element)
+                if node:
+                    self._model.RemoveNode(node)
+                    self.RefreshNode(node.parent, recursive=True)
+            elif action == 'rename':
+                node = self.GetDbNode(grassdb=grassdb,
+                                      location=location,
+                                      mapset=mapset,
+                                      map=map,
+                                      map_type=element)
+                if node:
+                    self._renameNode(node, newname)
+
     def _updateAfterMapsetChanged(self):
     def _updateAfterMapsetChanged(self):
         """Update tree after current mapset has changed"""
         """Update tree after current mapset has changed"""
         self.UpdateCurrentDbLocationMapsetNode()
         self.UpdateCurrentDbLocationMapsetNode()
         self.ExpandCurrentMapset()
         self.ExpandCurrentMapset()
         self.RefreshItems()
         self.RefreshItems()
+        self.ScheduleWatchCurrentMapset()
 
 
     def OnMetadata(self, event):
     def OnMetadata(self, event):
         """Show metadata of any raster/vector/3draster"""
         """Show metadata of any raster/vector/3draster"""

+ 23 - 73
gui/wxpython/lmgr/frame.py

@@ -69,7 +69,11 @@ from lmgr.giface import LayerManagerGrassInterface
 from datacatalog.catalog import DataCatalog
 from datacatalog.catalog import DataCatalog
 from gui_core.forms import GUI
 from gui_core.forms import GUI
 from gui_core.wrap import Menu, TextEntryDialog
 from gui_core.wrap import Menu, TextEntryDialog
-from startup.guiutils import switch_mapset_interactively
+from startup.guiutils import (
+    switch_mapset_interactively,
+    create_mapset_interactively,
+    create_location_interactively
+)
 
 
 
 
 class GMFrame(wx.Frame):
 class GMFrame(wx.Frame):
@@ -480,56 +484,15 @@ class GMFrame(wx.Frame):
 
 
     def OnLocationWizard(self, event):
     def OnLocationWizard(self, event):
         """Launch location wizard"""
         """Launch location wizard"""
-        from location_wizard.wizard import LocationWizard
-        from location_wizard.dialogs import RegionDef
-
-        gWizard = LocationWizard(parent=self,
-                                 grassdatabase=grass.gisenv()['GISDBASE'])
-        location = gWizard.location
-
-        if location is not None:
-            dlg = wx.MessageDialog(parent=self,
-                                   message=_('Location <%s> created.\n\n'
-                                             'Do you want to switch to the '
-                                             'new location?') % location,
-                                   caption=_("Switch to new location?"),
-                                   style=wx.YES_NO | wx.NO_DEFAULT |
-                                   wx.ICON_QUESTION | wx.CENTRE)
-
-            ret = dlg.ShowModal()
-            dlg.Destroy()
-            if ret == wx.ID_YES:
-                if RunCommand('g.mapset', parent=self,
-                              location=location,
-                              mapset='PERMANENT') != 0:
-                    return
-
-                # close current workspace and create new one
-                self.OnWorkspaceClose()
-                self.OnWorkspaceNew()
-                GMessage(parent=self,
-                         message=_("Current location is <%(loc)s>.\n"
-                                   "Current mapset is <%(mapset)s>.") %
-                         {'loc': location, 'mapset': 'PERMANENT'})
-
-                # code duplication with gis_set.py
-                dlg = wx.MessageDialog(
-                    parent=self,
-                    message=_(
-                        "Do you want to set the default "
-                        "region extents and resolution now?"),
-                    caption=_("Location <%s> created") %
-                    location,
-                    style=wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION)
-                dlg.CenterOnScreen()
-                if dlg.ShowModal() == wx.ID_YES:
-                    dlg.Destroy()
-                    defineRegion = RegionDef(self, location=location)
-                    defineRegion.CenterOnScreen()
-                    defineRegion.ShowModal()
-                    defineRegion.Destroy()
-                else:
-                    dlg.Destroy()
+        gisenv = grass.gisenv()
+        grassdb, location, mapset = (
+            create_location_interactively(self, gisenv['GISDBASE'])
+        )
+        if location:
+            self._giface.grassdbChanged.emit(grassdb=gisenv['GISDBASE'],
+                                             location=location,
+                                             action='new',
+                                             element='location')
 
 
     def OnSettingsChanged(self):
     def OnSettingsChanged(self):
         """Here can be functions which have to be called
         """Here can be functions which have to be called
@@ -1088,28 +1051,15 @@ class GMFrame(wx.Frame):
 
 
     def OnCreateMapset(self, event):
     def OnCreateMapset(self, event):
         """Create new mapset"""
         """Create new mapset"""
-        dlg = wx.TextEntryDialog(parent=self,
-                                 message=_('Enter name for new mapset:'),
-                                 caption=_('Create new mapset'))
-
-        if dlg.ShowModal() == wx.ID_OK:
-            mapset = dlg.GetValue()
-            if not mapset:
-                GError(parent=self,
-                       message=_("No mapset provided. Operation canceled."))
-                return
-
-            ret = RunCommand('g.mapset',
-                             parent=self,
-                             flags='c',
-                             mapset=mapset)
-            # ensure that DB connection is defined
-            ret += RunCommand('db.connect',
-                              parent=self,
-                              flags='c')
-            if ret == 0:
-                GMessage(parent=self,
-                         message=_("Current mapset is <%s>.") % mapset)
+        gisenv = grass.gisenv()
+        mapset = create_mapset_interactively(self, gisenv['GISDBASE'],
+                                             gisenv['LOCATION_NAME'])
+        if mapset:
+            self._giface.grassdbChanged.emit(grassdb=gisenv['GISDBASE'],
+                                             location=gisenv['LOCATION_NAME'],
+                                             mapset=mapset,
+                                             action='new',
+                                             element='mapset')
 
 
     def OnChangeMapset(self, event):
     def OnChangeMapset(self, event):
         """Change current mapset"""
         """Change current mapset"""

+ 13 - 0
gui/wxpython/lmgr/giface.py

@@ -184,12 +184,25 @@ class LayerManagerGrassInterface(object):
         self.lmgr = lmgr
         self.lmgr = lmgr
 
 
         # Signal when some map is created or updated by a module.
         # Signal when some map is created or updated by a module.
+        # Used for adding/refreshing displayed layers.
         # attributes: name: map name, ltype: map type,
         # attributes: name: map name, ltype: map type,
         # add: if map should be added to layer tree (questionable attribute)
         # add: if map should be added to layer tree (questionable attribute)
         self.mapCreated = Signal('LayerManagerGrassInterface.mapCreated')
         self.mapCreated = Signal('LayerManagerGrassInterface.mapCreated')
 
 
+        # Signal for communicating current mapset has been switched
         self.currentMapsetChanged = Signal('LayerManagerGrassInterface.currentMapsetChanged')
         self.currentMapsetChanged = Signal('LayerManagerGrassInterface.currentMapsetChanged')
 
 
+        # Signal for communicating something in current grassdb has changed.
+        # Parameters:
+        # action: required, is one of 'new', 'rename', 'delete'
+        # element: required, can be one of 'grassdb', 'location', 'mapset', 'raster', 'vector' and 'raster_3d'
+        # grassdb: path to grass db, required
+        # location: location name, required
+        # mapset: mapset name, required when element is 'mapset', 'raster', 'vector' or 'raster_3d'
+        # map: map name, required when element is 'raster', 'vector' or 'raster_3d'
+        # newname: new name (of mapset, map), required with action='rename'
+        self.grassdbChanged = Signal('LayerManagerGrassInterface.grassdbChanged')
+
         # Signal emitted to request updating of map
         # Signal emitted to request updating of map
         self.updateMap = Signal('LayerManagerGrassInterface.updateMap')
         self.updateMap = Signal('LayerManagerGrassInterface.updateMap')
 
 

+ 56 - 0
gui/wxpython/lmgr/layertree.py

@@ -238,6 +238,8 @@ class LayerTree(treemixin.DragAndDrop, CT.CustomTreeCtrl):
         self.Bind(wx.EVT_IDLE, self.OnIdle)
         self.Bind(wx.EVT_IDLE, self.OnIdle)
         self.Bind(wx.EVT_MOTION, self.OnMotion)
         self.Bind(wx.EVT_MOTION, self.OnMotion)
 
 
+        self._giface.grassdbChanged.connect(self.OnGrassDBChanged)
+
     def _setIcons(self, il):
     def _setIcons(self, il):
         self._icon = {}
         self._icon = {}
         for iconName in ("layerRaster", "layerRaster_3d", "layerRgb",
         for iconName in ("layerRaster", "layerRaster_3d", "layerRgb",
@@ -1314,6 +1316,60 @@ class LayerTree(treemixin.DragAndDrop, CT.CustomTreeCtrl):
 
 
         event.Skip()
         event.Skip()
 
 
+    def OnGrassDBChanged(self, action, element, grassdb, location,
+                         mapset=None, map=None, newname=None):
+        """Handler of giface.grassDbChanged signal, updates layers in tree.
+         Covers cases when map or mapset is deleted or renamed."""
+        gisenv = grass.gisenv()
+        if not (gisenv['GISDBASE'] == grassdb
+                and gisenv['LOCATION_NAME'] == location):
+            return
+        if action not in ('delete', 'rename'):
+            return
+        if element in ('raster', 'vector', 'raster_3d'):
+            name = map + '@' + mapset if '@' not in map else map
+            items = self.FindItemByData(key='name', value=name)
+            if items:
+                for item in reversed(items):
+                    if action == 'delete':
+                        self.Delete(item)
+                    elif action == 'rename':
+                        # rename in command
+                        cmd = self.GetLayerInfo(item, key='cmd')
+                        for i in range(1, len(cmd)):
+                            if map in cmd[i].split('=')[-1]:
+                                cmd[i] = cmd[i].replace(map, newname)
+                        self.SetLayerInfo(item, key='cmd', value=cmd)
+                        self.ChangeLayer(item)
+                        # change label if not edited
+                        label = self.GetLayerInfo(item, key='label')
+                        if not label:
+                            newlabel = self.GetItemText(item).replace(map, newname)
+                            self.SetItemText(item, newlabel)
+        elif element == 'mapset':
+            items = []
+            item = self.GetFirstChild(self.root)[0]
+            while item and item.IsOk():
+                cmd = self.GetLayerInfo(item, key='cmd')
+                for each in cmd:
+                    if '@' + mapset in each:
+                        items.append(item)
+                        break
+                item = self.GetNextItem(item)
+            for item in reversed(items):
+                if action == 'delete':
+                    self.Delete(item)
+                elif action == 'rename':
+                    # rename in command
+                    cmd = self.GetLayerInfo(item, key='cmd')
+                    for i in range(1, len(cmd)):
+                        if mapset in cmd[i].split('=')[-1]:
+                            cmd[i] = cmd[i].replace(mapset, newname)
+                    self.SetLayerInfo(item, key='cmd', value=cmd)
+                    self.ChangeLayer(item)
+                    newlabel = self.GetItemText(item).replace('@' + mapset, '@' + newname)
+                    self.SetItemText(item, newlabel)
+
     def AddLayer(self, ltype, lname=None, lchecked=None, lopacity=1.0,
     def AddLayer(self, ltype, lname=None, lchecked=None, lopacity=1.0,
                  lcmd=None, lgroup=None, lvdigit=None, lnviz=None,
                  lcmd=None, lgroup=None, lvdigit=None, lnviz=None,
                  multiple=True, loadWorkspace=False):
                  multiple=True, loadWorkspace=False):

+ 7 - 0
gui/wxpython/modules/mcalc_builder.py

@@ -716,6 +716,13 @@ class MapCalcFrame(wx.Frame):
             ltype = 'raster_3d'
             ltype = 'raster_3d'
         self._giface.mapCreated.emit(
         self._giface.mapCreated.emit(
             name=name, ltype=ltype, add=self.addbox.IsChecked())
             name=name, ltype=ltype, add=self.addbox.IsChecked())
+        gisenv = grass.gisenv()
+        self._giface.grassdbChanged.emit(grassdb=gisenv['GISDBASE'],
+                                         location=gisenv['LOCATION_NAME'],
+                                         mapset=gisenv['MAPSET'],
+                                         action='new',
+                                         map=name.split('@')[0],
+                                         element=ltype)
 
 
     def OnSaveExpression(self, event):
     def OnSaveExpression(self, event):
         """Saves expression to file
         """Saves expression to file

+ 7 - 0
gui/wxpython/rdigit/controller.py

@@ -452,6 +452,13 @@ class RDigitController(wx.EvtHandler):
         self._editedRaster = name
         self._editedRaster = name
         self._mapType = mapType
         self._mapType = mapType
         self.newRasterCreated.emit(name=name)
         self.newRasterCreated.emit(name=name)
+        gisenv = gcore.gisenv()
+        self._giface.grassdbChanged.emit(grassdb=gisenv['GISDBASE'],
+                                         location=gisenv['LOCATION_NAME'],
+                                         mapset=gisenv['MAPSET'],
+                                         action='new',
+                                         map=name.split('@')[0],
+                                         element='raster')
 
 
     def _backupRaster(self, name):
     def _backupRaster(self, name):
         """Creates a temporary backup raster necessary for undo behavior.
         """Creates a temporary backup raster necessary for undo behavior.