Bläddra i källkod

wxGUI: Replace startup screen by fallback session (#1400)

* Startup screen code removed. Used only when 
* Either demo/default world location is used (first time user) or an XY location temporary location is used (fallback).
* Last mapset path is now noted in gisrc of the current session. Used in GUI to offer switch from fallback to locked.
* non_standard_startup and first_time_user in grassdb/checks.py.
* grassdb/config for special strings such as `<UNKNOWN>`.
* Fallback tmp location only for GUI. Default currently for both GUI and CLI (`--text`).
* Call set_mapset in every branch (all paths lead to the same series of checks and setup).
* This increases the need for refactoring of `main()` and needs removal of `--gtext`.
Linda Kladivova 4 år sedan
förälder
incheckning
8fe13636b7

+ 1 - 1
gui/wxpython/Makefile

@@ -14,7 +14,7 @@ SRCFILES := $(wildcard icons/*.py scripts/*.py xml/*) \
 	mapswipe/*.py modules/*.py nviz/*.py psmap/*.py rdigit/*.py \
 	mapswipe/*.py modules/*.py nviz/*.py psmap/*.py rdigit/*.py \
 	rlisetup/*.py startup/*.py timeline/*.py vdigit/*.py \
 	rlisetup/*.py startup/*.py timeline/*.py vdigit/*.py \
 	vnet/*.py web_services/*.py wxplot/*.py iscatt/*.py tplot/*.py photo2image/*.py image2target/*.py) \
 	vnet/*.py web_services/*.py wxplot/*.py iscatt/*.py tplot/*.py photo2image/*.py image2target/*.py) \
-	gis_set.py gis_set_error.py wxgui.py README
+	wxgui.py README
 
 
 DSTFILES := $(patsubst %,$(DSTDIR)/%,$(SRCFILES)) \
 DSTFILES := $(patsubst %,$(DSTDIR)/%,$(SRCFILES)) \
 	$(patsubst %.py,$(DSTDIR)/%.pyc,$(filter %.py,$(SRCFILES)))
 	$(patsubst %.py,$(DSTDIR)/%.pyc,$(filter %.py,$(SRCFILES)))

+ 39 - 4
gui/wxpython/datacatalog/catalog.py

@@ -26,10 +26,14 @@ from gui_core.infobar import InfoBar
 from datacatalog.infomanager import DataCatalogInfoManager
 from datacatalog.infomanager import DataCatalogInfoManager
 from gui_core.wrap import Menu
 from gui_core.wrap import Menu
 from gui_core.forms import GUI
 from gui_core.forms import GUI
+from grass.script import gisenv
 
 
 from grass.pydispatch.signal import Signal
 from grass.pydispatch.signal import Signal
 
 
-from grass.grassdb.checks import is_current_mapset_in_demolocation
+from grass.grassdb.manage import split_mapset_path
+from grass.grassdb.checks import (get_reason_id_mapset_not_usable,
+                          is_fallback_session,
+                          is_first_time_user)
 
 
 
 
 class DataCatalog(wx.Panel):
 class DataCatalog(wx.Panel):
@@ -55,7 +59,9 @@ class DataCatalog(wx.Panel):
         self.tree.showNotification.connect(self.showNotification)
         self.tree.showNotification.connect(self.showNotification)
 
 
         # infobar for data catalog
         # infobar for data catalog
+        delay = 2000
         self.infoBar = InfoBar(self)
         self.infoBar = InfoBar(self)
+        self.giface.currentMapsetChanged.connect(self.dismissInfobar)
 
 
         # infobar manager for data catalog
         # infobar manager for data catalog
         self.infoManager = DataCatalogInfoManager(infobar=self.infoBar,
         self.infoManager = DataCatalogInfoManager(infobar=self.infoBar,
@@ -65,9 +71,22 @@ class DataCatalog(wx.Panel):
         # some layout
         # some layout
         self._layout()
         self._layout()
 
 
-        # show data structure infobar for first-time user with proper layout
-        if is_current_mapset_in_demolocation():
-            wx.CallLater(2000, self.showDataStructureInfo)
+        # show infobar for first-time user if applicable
+        if is_first_time_user():
+            # show data structure infobar for first-time user
+            wx.CallLater(delay, self.showDataStructureInfo)
+
+        # show infobar if last used mapset is not usable
+        if is_fallback_session():
+            # get reason why last used mapset is not usable
+            last_mapset_path = gisenv()["LAST_MAPSET_PATH"]
+            self.reason_id = get_reason_id_mapset_not_usable(last_mapset_path)
+            if self.reason_id in ("non-existent", "invalid", "different-owner"):
+                # show non-standard situation info
+                wx.CallLater(delay, self.showFallbackSessionInfo)
+            elif self.reason_id == "locked":
+                # show info allowing to switch to locked mapset
+                wx.CallLater(delay, self.showLockedMapsetInfo)
 
 
     def _layout(self):
     def _layout(self):
         """Do layout"""
         """Do layout"""
@@ -85,12 +104,22 @@ class DataCatalog(wx.Panel):
     def showDataStructureInfo(self):
     def showDataStructureInfo(self):
         self.infoManager.ShowDataStructureInfo(self.OnCreateLocation)
         self.infoManager.ShowDataStructureInfo(self.OnCreateLocation)
 
 
+    def showLockedMapsetInfo(self):
+        self.infoManager.ShowLockedMapsetInfo(self.OnSwitchToLastUsedMapset)
+
+    def showFallbackSessionInfo(self):
+        self.infoManager.ShowFallbackSessionInfo(self.reason_id)
+
     def showImportDataInfo(self):
     def showImportDataInfo(self):
         self.infoManager.ShowImportDataInfo(self.OnImportOgrLayers, self.OnImportGdalLayers)
         self.infoManager.ShowImportDataInfo(self.OnImportOgrLayers, self.OnImportGdalLayers)
 
 
     def LoadItems(self):
     def LoadItems(self):
         self.tree.ReloadTreeItems()
         self.tree.ReloadTreeItems()
 
 
+    def dismissInfobar(self):
+        if self.infoBar.IsShown():
+            self.infoBar.Dismiss()
+
     def OnReloadTree(self, event):
     def OnReloadTree(self, event):
         """Reload whole tree"""
         """Reload whole tree"""
         self.LoadItems()
         self.LoadItems()
@@ -135,6 +164,12 @@ class DataCatalog(wx.Panel):
         db_node, loc_node, mapset_node = self.tree.GetCurrentDbLocationMapsetNode()
         db_node, loc_node, mapset_node = self.tree.GetCurrentDbLocationMapsetNode()
         self.tree.DownloadLocation(db_node)
         self.tree.DownloadLocation(db_node)
 
 
+    def OnSwitchToLastUsedMapset(self, event):
+        """Switch to last used mapset"""
+        last_mapset_path = gisenv()["LAST_MAPSET_PATH"]
+        grassdb, location, mapset = split_mapset_path(last_mapset_path)
+        self.tree.SwitchMapset(grassdb, location, mapset)
+
     def OnImportGdalLayers(self, event):
     def OnImportGdalLayers(self, event):
         """Convert multiple GDAL layers to GRASS raster map layers"""
         """Convert multiple GDAL layers to GRASS raster map layers"""
         from modules.import_export import GdalImportDialog
         from modules.import_export import GdalImportDialog

+ 53 - 6
gui/wxpython/datacatalog/infomanager.py

@@ -20,6 +20,7 @@ This program is free software under the GNU General Public License
 import wx
 import wx
 
 
 from grass.script import gisenv
 from grass.script import gisenv
+from grass.grassdb.checks import get_mapset_owner
 
 
 
 
 class DataCatalogInfoManager:
 class DataCatalogInfoManager:
@@ -31,8 +32,10 @@ class DataCatalogInfoManager:
 
 
     def ShowDataStructureInfo(self, onCreateLocationHandler):
     def ShowDataStructureInfo(self, onCreateLocationHandler):
         """Show info about the data hierarchy focused on the first-time user"""
         """Show info about the data hierarchy focused on the first-time user"""
-        buttons = [("Create new Location", onCreateLocationHandler),
-                   ("Learn More", self._onLearnMore)]
+        buttons = [
+            (_("Create new Location"), onCreateLocationHandler),
+            (_("Learn more"), self._onLearnMore),
+        ]
         message = _(
         message = _(
             "GRASS GIS helps you organize your data using Locations (projects) "
             "GRASS GIS helps you organize your data using Locations (projects) "
             "which contain Mapsets (subprojects). All data in one Location is "
             "which contain Mapsets (subprojects). All data in one Location is "
@@ -41,13 +44,15 @@ class DataCatalogInfoManager:
             "which uses WGS 84 (EPSG:4326). Consider creating a new Location with a CRS "
             "which uses WGS 84 (EPSG:4326). Consider creating a new Location with a CRS "
             "specific to your area. You can do it now or anytime later from "
             "specific to your area. You can do it now or anytime later from "
             "the toolbar above."
             "the toolbar above."
-        ).format(loc=gisenv()['LOCATION_NAME'])
+        ).format(loc=gisenv()["LOCATION_NAME"])
         self.infoBar.ShowMessage(message, wx.ICON_INFORMATION, buttons)
         self.infoBar.ShowMessage(message, wx.ICON_INFORMATION, buttons)
 
 
     def ShowImportDataInfo(self, OnImportOgrLayersHandler, OnImportGdalLayersHandler):
     def ShowImportDataInfo(self, OnImportOgrLayersHandler, OnImportGdalLayersHandler):
         """Show info about the data import focused on the first-time user"""
         """Show info about the data import focused on the first-time user"""
-        buttons = [("Import vector data", OnImportOgrLayersHandler),
-                   ("Import raster data", OnImportGdalLayersHandler)]
+        buttons = [
+            (_("Import vector data"), OnImportOgrLayersHandler),
+            (_("Import raster data"), OnImportGdalLayersHandler),
+        ]
         message = _(
         message = _(
             "You have successfully created a new Location {loc}. "
             "You have successfully created a new Location {loc}. "
             "Currently you are in its PERMANENT Mapset which is used for "
             "Currently you are in its PERMANENT Mapset which is used for "
@@ -55,8 +60,50 @@ class DataCatalogInfoManager:
             "Mapsets. You can create new Mapsets for different tasks by right "
             "Mapsets. You can create new Mapsets for different tasks by right "
             "clicking on the Location name.\n\n"
             "clicking on the Location name.\n\n"
             "To import data, go to the toolbar above or use the buttons below."
             "To import data, go to the toolbar above or use the buttons below."
-        ).format(loc=gisenv()['LOCATION_NAME'])
+        ).format(loc=gisenv()["LOCATION_NAME"])
         self.infoBar.ShowMessage(message, wx.ICON_INFORMATION, buttons)
         self.infoBar.ShowMessage(message, wx.ICON_INFORMATION, buttons)
 
 
+    def ShowFallbackSessionInfo(self, reason_id):
+        """Show info when last used mapset is not usable"""
+        string = self._text_from_reason_id(reason_id)
+        message = _(
+            "{string} GRASS GIS has started in a temporary Location. "
+            "To continue, use Data Catalog below to switch to a different Location."
+        ).format(
+            string=string,
+        )
+        self.infoBar.ShowMessage(message, wx.ICON_INFORMATION)
+
+    def ShowLockedMapsetInfo(self, OnSwitchMapsetHandler):
+        """Show info when last used mapset is locked"""
+        last_used_mapset_path = gisenv()["LAST_MAPSET_PATH"]
+        buttons = [(_("Switch to last used mapset"), OnSwitchMapsetHandler)]
+        message = _(
+            "Last used mapset in path '{mapsetpath}' is currently in use. "
+            "GRASS GIS has started in a temporary Location. "
+            "To continue, use Data Catalog below to switch to a different Location "
+            "or remove lock file and switch to the last used mapset."
+        ).format(mapsetpath=last_used_mapset_path)
+        self.infoBar.ShowMessage(message, wx.ICON_INFORMATION, buttons)
+
+    def _text_from_reason_id(self, reason_id):
+        """ Get string for infobar message based on the reason."""
+        last_used_mapset_path = gisenv()["LAST_MAPSET_PATH"]
+        reason = None
+        if reason_id == "non-existent":
+            reason = _(
+                "Last used mapset in path '{mapsetpath}' does not exist."
+            ).format(mapsetpath=last_used_mapset_path)
+        elif reason_id == "invalid":
+            reason = _("Last used mapset in path '{mapsetpath}' is invalid.").format(
+                mapsetpath=last_used_mapset_path
+            )
+        elif reason_id == "different-owner":
+            owner = get_mapset_owner(last_used_mapset_path)
+            reason = _(
+                "Last used mapset in path '{mapsetpath}' has different owner {owner}."
+            ).format(owner=owner, mapsetpath=last_used_mapset_path)
+        return reason
+
     def _onLearnMore(self, event):
     def _onLearnMore(self, event):
         self._giface.Help(entry="grass_database")
         self._giface.Help(entry="grass_database")

+ 11 - 7
gui/wxpython/datacatalog/tree.py

@@ -74,8 +74,7 @@ import grass.script as gscript
 from grass.script import gisenv
 from grass.script import gisenv
 from grass.grassdb.data import map_exists
 from grass.grassdb.data import map_exists
 from grass.grassdb.checks import (get_mapset_owner, is_mapset_locked,
 from grass.grassdb.checks import (get_mapset_owner, is_mapset_locked,
-                                  is_different_mapset_owner,
-                                  is_current_mapset_in_demolocation)
+                                  is_different_mapset_owner, is_first_time_user)
 from grass.exceptions import CalledModuleError
 from grass.exceptions import CalledModuleError
 
 
 
 
@@ -957,7 +956,7 @@ class DataCatalogTree(TreeView):
         """
         """
         Creates new location interactively and adds it to the tree and switch
         Creates new location interactively and adds it to the tree and switch
         to its new PERMANENT mapset.
         to its new PERMANENT mapset.
-        If a user was in Demolocation, it shows data import infobar.
+        If a user is a first-time user, it shows data import infobar.
         """
         """
         grassdatabase, location, mapset = (
         grassdatabase, location, mapset = (
             create_location_interactively(self, grassdb_node.data['name'])
             create_location_interactively(self, grassdb_node.data['name'])
@@ -968,14 +967,14 @@ class DataCatalogTree(TreeView):
                                              element='location',
                                              element='location',
                                              action='new')
                                              action='new')
 
 
-            # show data import infobar for first-time user with proper layout
-            if is_current_mapset_in_demolocation():
-                self.showImportDataInfo.emit()
-
             # switch to PERMANENT mapset in newly created location
             # switch to PERMANENT mapset in newly created location
             self.SwitchMapset(grassdatabase, location, mapset,
             self.SwitchMapset(grassdatabase, location, mapset,
                               show_confirmation=True)
                               show_confirmation=True)
 
 
+            # show data import infobar for first-time user with proper layout
+            if is_first_time_user():
+                self.showImportDataInfo.emit()
+
     def OnCreateLocation(self, event):
     def OnCreateLocation(self, event):
         """Create new location"""
         """Create new location"""
         self.CreateLocation(self.selected_grassdb[0])
         self.CreateLocation(self.selected_grassdb[0])
@@ -1560,6 +1559,11 @@ class DataCatalogTree(TreeView):
                                       location=location)
                                       location=location)
                 if node:
                 if node:
                     self._renameNode(node, newname)
                     self._renameNode(node, newname)
+        elif element == 'grassdb':
+            if action == 'delete':
+                node = self.GetDbNode(grassdb=grassdb)
+                if node:
+                    self.RemoveGrassDB(node)
         elif element in ('raster', 'vector', 'raster_3d'):
         elif element in ('raster', 'vector', 'raster_3d'):
             # when watchdog is used, it watches current mapset,
             # when watchdog is used, it watches current mapset,
             # so we don't process any signals here,
             # so we don't process any signals here,

+ 0 - 955
gui/wxpython/gis_set.py

@@ -1,955 +0,0 @@
-"""
-@package gis_set
-
-GRASS start-up screen.
-
-Initialization module for wxPython GRASS GUI.
-Location/mapset management (selection, creation, etc.).
-
-Classes:
- - gis_set::GRASSStartup
- - gis_set::GListBox
- - gis_set::StartUp
-
-(C) 2006-2014 by the GRASS Development Team
-
-This program is free software under the GNU General Public License
-(>=v2). Read the file COPYING that comes with GRASS for details.
-
-@author Michael Barton and Jachym Cepicky (original author)
-@author Martin Landa <landa.martin gmail.com> (various updates)
-"""
-
-import os
-import sys
-import copy
-import platform
-
-# i18n is taken care of in the grass library code.
-# So we need to import it before any of the GUI code.
-
-from core import globalvar
-import wx
-# import adv and html before wx.App is created, otherwise
-# we get annoying "Debug: Adding duplicate image handler for 'Windows bitmap file'"
-# during download location dialog start up, remove when not needed
-import wx.adv
-import wx.html
-import wx.lib.mixins.listctrl as listmix
-
-from grass.grassdb.checks import get_lockfile_if_present
-from grass.app import get_possible_database_path
-
-from core.gcmd import GError, RunCommand
-from core.utils import GetListOfLocations, GetListOfMapsets
-from startup.guiutils import (SetSessionMapset,
-                              create_mapset_interactively,
-                              create_location_interactively,
-                              rename_mapset_interactively,
-                              rename_location_interactively,
-                              delete_mapset_interactively,
-                              delete_location_interactively,
-                              download_location_interactively)
-import startup.guiutils as sgui
-from gui_core.widgets import StaticWrapText
-from gui_core.wrap import Button, ListCtrl, StaticText, StaticBox, \
-    TextCtrl, BitmapFromImage
-
-
-class GRASSStartup(wx.Frame):
-    exit_success = 0
-    # 2 is file not found from python interpreter
-    exit_user_requested = 5
-
-    """GRASS start-up screen"""
-
-    def __init__(self, parent=None, id=wx.ID_ANY,
-                 style=wx.DEFAULT_FRAME_STYLE):
-
-        #
-        # GRASS variables
-        #
-        self.gisbase = os.getenv("GISBASE")
-        self.grassrc = sgui.read_gisrc()
-        self.gisdbase = self.GetRCValue("GISDBASE")
-
-        #
-        # list of locations/mapsets
-        #
-        self.listOfLocations = []
-        self.listOfMapsets = []
-        self.listOfMapsetsSelectable = []
-
-        wx.Frame.__init__(self, parent=parent, id=id, style=style)
-
-        self.locale = wx.Locale(language=wx.LANGUAGE_DEFAULT)
-
-        # scroll panel was used here but not properly and is probably not need
-        # as long as it is not high too much
-        self.panel = wx.Panel(parent=self, id=wx.ID_ANY)
-
-        # i18N
-
-        #
-        # graphical elements
-        #
-        # image
-        try:
-            if os.getenv('ISISROOT'):
-                name = os.path.join(
-                    globalvar.GUIDIR,
-                    "images",
-                    "startup_banner_isis.png")
-            else:
-                name = os.path.join(
-                    globalvar.GUIDIR, "images", "startup_banner.png")
-            self.hbitmap = wx.StaticBitmap(self.panel, wx.ID_ANY,
-                                           wx.Bitmap(name=name,
-                                                     type=wx.BITMAP_TYPE_PNG))
-        except:
-            self.hbitmap = wx.StaticBitmap(
-                self.panel, wx.ID_ANY, BitmapFromImage(
-                    wx.EmptyImage(530, 150)))
-
-        # labels
-        # crashes when LOCATION doesn't exist
-        # get version & revision
-        grassVersion, grassRevisionStr = sgui.GetVersion()
-
-        self.gisdbase_box = StaticBox(
-            parent=self.panel, id=wx.ID_ANY, label=" %s " %
-            _("1. Select GRASS GIS database directory"))
-        self.location_box = StaticBox(
-            parent=self.panel, id=wx.ID_ANY, label=" %s " %
-            _("2. Select GRASS Location"))
-        self.mapset_box = StaticBox(
-            parent=self.panel, id=wx.ID_ANY, label=" %s " %
-            _("3. Select GRASS Mapset"))
-
-        self.lmessage = StaticWrapText(parent=self.panel)
-        # It is not clear if all wx versions supports color, so try-except.
-        # The color itself may not be correct for all platforms/system settings
-        # but in http://xoomer.virgilio.it/infinity77/wxPython/Widgets/wx.SystemSettings.html
-        # there is no 'warning' color.
-        try:
-            self.lmessage.SetForegroundColour(wx.Colour(255, 0, 0))
-        except AttributeError:
-            pass
-
-        self.gisdbase_panel = wx.Panel(parent=self.panel)
-        self.location_panel = wx.Panel(parent=self.panel)
-        self.mapset_panel = wx.Panel(parent=self.panel)
-
-        self.ldbase = StaticText(
-            parent=self.gisdbase_panel, id=wx.ID_ANY,
-            label=_("GRASS GIS database directory contains Locations."))
-
-        self.llocation = StaticWrapText(
-            parent=self.location_panel, id=wx.ID_ANY,
-            label=_("All data in one Location is in the same "
-                    " coordinate reference system (projection)."
-                    " One Location can be one project."
-                    " Location contains Mapsets."),
-            style=wx.ALIGN_LEFT)
-
-        self.lmapset = StaticWrapText(
-            parent=self.mapset_panel, id=wx.ID_ANY,
-            label=_("Mapset contains GIS data related"
-                    " to one project, task within one project,"
-                    " subregion or user."),
-            style=wx.ALIGN_LEFT)
-
-        try:
-            for label in [self.ldbase, self.llocation, self.lmapset]:
-                label.SetForegroundColour(
-                    wx.SystemSettings.GetColour(wx.SYS_COLOUR_GRAYTEXT))
-        except AttributeError:
-            # for explanation of try-except see above
-            pass
-
-        # buttons
-        self.bstart = Button(parent=self.panel, id=wx.ID_ANY,
-                             label=_("Start &GRASS session"))
-        self.bstart.SetDefault()
-        self.bexit = Button(parent=self.panel, id=wx.ID_EXIT)
-        self.bstart.SetMinSize((180, self.bexit.GetSize()[1]))
-        self.bhelp = Button(parent=self.panel, id=wx.ID_HELP)
-        self.bbrowse = Button(parent=self.gisdbase_panel, id=wx.ID_ANY,
-                              label=_("&Browse"))
-        self.bmapset = Button(parent=self.mapset_panel, id=wx.ID_ANY,
-                              # GTC New mapset
-                              label=_("&New"))
-        self.bmapset.SetToolTip(_("Create a new Mapset in selected Location"))
-        self.bwizard = Button(parent=self.location_panel, id=wx.ID_ANY,
-                              # GTC New location
-                              label=_("N&ew"))
-        self.bwizard.SetToolTip(
-            _(
-                "Create a new location using location wizard."
-                " After location is created successfully,"
-                " GRASS session is started."))
-        self.rename_location_button = Button(parent=self.location_panel, id=wx.ID_ANY,
-                                             # GTC Rename location
-                                             label=_("Ren&ame"))
-        self.rename_location_button.SetToolTip(_("Rename selected location"))
-        self.delete_location_button = Button(parent=self.location_panel, id=wx.ID_ANY,
-                                             # GTC Delete location
-                                             label=_("De&lete"))
-        self.delete_location_button.SetToolTip(_("Delete selected location"))
-        self.download_location_button = Button(parent=self.location_panel, id=wx.ID_ANY,
-                                             label=_("Do&wnload"))
-        self.download_location_button.SetToolTip(_("Download sample location"))
-
-        self.rename_mapset_button = Button(parent=self.mapset_panel, id=wx.ID_ANY,
-                                           # GTC Rename mapset
-                                           label=_("&Rename"))
-        self.rename_mapset_button.SetToolTip(_("Rename selected mapset"))
-        self.delete_mapset_button = Button(parent=self.mapset_panel, id=wx.ID_ANY,
-                                           # GTC Delete mapset
-                                           label=_("&Delete"))
-        self.delete_mapset_button.SetToolTip(_("Delete selected mapset"))
-
-        # textinputs
-        self.tgisdbase = TextCtrl(
-            parent=self.gisdbase_panel, id=wx.ID_ANY, value="", size=(
-                300, -1), style=wx.TE_PROCESS_ENTER)
-
-        # Locations
-        self.lblocations = GListBox(parent=self.location_panel,
-                                    id=wx.ID_ANY, size=(180, 200),
-                                    choices=self.listOfLocations)
-        self.lblocations.SetColumnWidth(0, 180)
-
-        # TODO: sort; but keep PERMANENT on top of list
-        # Mapsets
-        self.lbmapsets = GListBox(parent=self.mapset_panel,
-                                  id=wx.ID_ANY, size=(180, 200),
-                                  choices=self.listOfMapsets)
-        self.lbmapsets.SetColumnWidth(0, 180)
-
-        # layout & properties, first do layout so everything is created
-        self._do_layout()
-        self._set_properties(grassVersion, grassRevisionStr)
-
-        # events
-        self.bbrowse.Bind(wx.EVT_BUTTON, self.OnBrowse)
-        self.bstart.Bind(wx.EVT_BUTTON, self.OnStart)
-        self.bexit.Bind(wx.EVT_BUTTON, self.OnExit)
-        self.bhelp.Bind(wx.EVT_BUTTON, self.OnHelp)
-        self.bmapset.Bind(wx.EVT_BUTTON, self.OnCreateMapset)
-        self.bwizard.Bind(wx.EVT_BUTTON, self.OnCreateLocation)
-
-        self.rename_location_button.Bind(wx.EVT_BUTTON, self.OnRenameLocation)
-        self.delete_location_button.Bind(wx.EVT_BUTTON, self.OnDeleteLocation)
-        self.download_location_button.Bind(wx.EVT_BUTTON, self.OnDownloadLocation)
-        self.rename_mapset_button.Bind(wx.EVT_BUTTON, self.OnRenameMapset)
-        self.delete_mapset_button.Bind(wx.EVT_BUTTON, self.OnDeleteMapset)
-
-        self.lblocations.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnSelectLocation)
-        self.lbmapsets.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnSelectMapset)
-        self.lbmapsets.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.OnStart)
-        self.tgisdbase.Bind(wx.EVT_TEXT_ENTER, self.OnSetDatabase)
-        self.Bind(wx.EVT_CLOSE, self.OnCloseWindow)
-
-    def _set_properties(self, version, revision):
-        """Set frame properties
-
-        :param version: Version in the form of X.Y.Z
-        :param revision: Version control revision with leading space
-
-        *revision* should be an empty string in case of release and
-        otherwise it needs a leading space to be separated from the rest
-        of the title.
-        """
-        self.SetTitle(_("GRASS GIS %s Startup%s") % (version, revision))
-        self.SetIcon(wx.Icon(os.path.join(globalvar.ICONDIR, "grass.ico"),
-                             wx.BITMAP_TYPE_ICO))
-
-        self.bstart.SetToolTip(_("Enter GRASS session"))
-        self.bstart.Enable(False)
-        self.bmapset.Enable(False)
-        # this all was originally a choice, perhaps just mapset needed
-        self.rename_location_button.Enable(False)
-        self.delete_location_button.Enable(False)
-        self.rename_mapset_button.Enable(False)
-        self.delete_mapset_button.Enable(False)
-
-        # set database
-        if not self.gisdbase:
-            # sets an initial path for gisdbase if nothing in GISRC
-            if os.path.isdir(os.getenv("HOME")):
-                self.gisdbase = os.getenv("HOME")
-            else:
-                self.gisdbase = os.getcwd()
-        try:
-            self.tgisdbase.SetValue(self.gisdbase)
-        except UnicodeDecodeError:
-            wx.MessageBox(parent=self, caption=_("Error"),
-                          message=_("Unable to set GRASS database. "
-                                    "Check your locale settings."),
-                          style=wx.OK | wx.ICON_ERROR | wx.CENTRE)
-
-        self.OnSetDatabase(None)
-        location = self.GetRCValue("LOCATION_NAME")
-        if location == "<UNKNOWN>" or location is None:
-            return
-        if not os.path.isdir(os.path.join(self.gisdbase, location)):
-            location = None
-
-        # list of locations
-        self.UpdateLocations(self.gisdbase)
-        try:
-            self.lblocations.SetSelection(self.listOfLocations.index(location),
-                                          force=True)
-            self.lblocations.EnsureVisible(
-                self.listOfLocations.index(location))
-        except ValueError:
-            sys.stderr.write(
-                _("ERROR: Location <%s> not found\n") %
-                self.GetRCValue("LOCATION_NAME"))
-            if len(self.listOfLocations) > 0:
-                self.lblocations.SetSelection(0, force=True)
-                self.lblocations.EnsureVisible(0)
-                location = self.listOfLocations[0]
-            else:
-                return
-
-        # list of mapsets
-        self.UpdateMapsets(os.path.join(self.gisdbase, location))
-        mapset = self.GetRCValue("MAPSET")
-        if mapset:
-            try:
-                self.lbmapsets.SetSelection(self.listOfMapsets.index(mapset),
-                                            force=True)
-                self.lbmapsets.EnsureVisible(self.listOfMapsets.index(mapset))
-            except ValueError:
-                sys.stderr.write(_("ERROR: Mapset <%s> not found\n") % mapset)
-                self.lbmapsets.SetSelection(0, force=True)
-                self.lbmapsets.EnsureVisible(0)
-
-    def _do_layout(self):
-        sizer = wx.BoxSizer(wx.VERTICAL)
-        self.sizer = sizer  # for the layout call after changing message
-        dbase_sizer = wx.BoxSizer(wx.HORIZONTAL)
-
-        location_mapset_sizer = wx.BoxSizer(wx.HORIZONTAL)
-
-        gisdbase_panel_sizer = wx.BoxSizer(wx.VERTICAL)
-        gisdbase_boxsizer = wx.StaticBoxSizer(self.gisdbase_box, wx.VERTICAL)
-
-        btns_sizer = wx.BoxSizer(wx.HORIZONTAL)
-
-        self.gisdbase_panel.SetSizer(gisdbase_panel_sizer)
-
-        # gis data directory
-
-        gisdbase_boxsizer.Add(self.gisdbase_panel, proportion=1,
-                              flag=wx.EXPAND | wx.ALL,
-                              border=1)
-
-        gisdbase_panel_sizer.Add(dbase_sizer, proportion=1,
-                                 flag=wx.EXPAND | wx.ALL,
-                                 border=1)
-        gisdbase_panel_sizer.Add(self.ldbase, proportion=0,
-                                 flag=wx.EXPAND | wx.ALL,
-                                 border=1)
-
-        dbase_sizer.Add(self.tgisdbase, proportion=1,
-                        flag=wx.ALIGN_CENTER_VERTICAL | wx.ALL,
-                        border=1)
-        dbase_sizer.Add(self.bbrowse, proportion=0,
-                        flag=wx.ALIGN_CENTER_VERTICAL | wx.ALL,
-                        border=1)
-
-        gisdbase_panel_sizer.Fit(self.gisdbase_panel)
-
-        # location and mapset lists
-
-        def layout_list_box(box, panel, list_box, buttons, description):
-            panel_sizer = wx.BoxSizer(wx.VERTICAL)
-            main_sizer = wx.BoxSizer(wx.HORIZONTAL)
-            box_sizer = wx.StaticBoxSizer(box, wx.VERTICAL)
-            buttons_sizer = wx.BoxSizer(wx.VERTICAL)
-
-            panel.SetSizer(panel_sizer)
-            panel_sizer.Fit(panel)
-
-            main_sizer.Add(list_box, proportion=1,
-                           flag=wx.EXPAND | wx.ALL,
-                           border=1)
-            main_sizer.Add(buttons_sizer, proportion=0,
-                           flag=wx.ALL,
-                           border=1)
-            for button in buttons:
-                buttons_sizer.Add(button, proportion=0,
-                                  flag=wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM,
-                                  border=3)
-            box_sizer.Add(panel, proportion=1,
-                          flag=wx.EXPAND | wx.ALL,
-                          border=1)
-            panel_sizer.Add(main_sizer, proportion=1,
-                            flag=wx.EXPAND | wx.ALL,
-                            border=1)
-            panel_sizer.Add(description, proportion=0,
-                            flag=wx.EXPAND | wx.ALL,
-                            border=1)
-            return box_sizer
-
-        location_boxsizer = layout_list_box(
-            box=self.location_box,
-            panel=self.location_panel,
-            list_box=self.lblocations,
-            buttons=[self.bwizard, self.rename_location_button,
-                     self.delete_location_button,
-                     self.download_location_button],
-            description=self.llocation)
-        mapset_boxsizer = layout_list_box(
-            box=self.mapset_box,
-            panel=self.mapset_panel,
-            list_box=self.lbmapsets,
-            buttons=[self.bmapset, self.rename_mapset_button,
-                     self.delete_mapset_button],
-            description=self.lmapset)
-
-        # location and mapset sizer
-        location_mapset_sizer.Add(location_boxsizer, proportion=1,
-                                  flag=wx.LEFT | wx.RIGHT | wx.EXPAND,
-                                  border=3)
-        location_mapset_sizer.Add(mapset_boxsizer, proportion=1,
-                                  flag=wx.RIGHT | wx.EXPAND,
-                                  border=3)
-
-        # buttons
-        btns_sizer.Add(self.bstart, proportion=0,
-                       flag=wx.ALIGN_CENTER_HORIZONTAL |
-                       wx.ALIGN_CENTER_VERTICAL |
-                       wx.ALL,
-                       border=5)
-        btns_sizer.Add(self.bexit, proportion=0,
-                       flag=wx.ALIGN_CENTER_HORIZONTAL |
-                       wx.ALIGN_CENTER_VERTICAL |
-                       wx.ALL,
-                       border=5)
-        btns_sizer.Add(self.bhelp, proportion=0,
-                       flag=wx.ALIGN_CENTER_HORIZONTAL |
-                       wx.ALIGN_CENTER_VERTICAL |
-                       wx.ALL,
-                       border=5)
-
-        # main sizer
-        sizer.Add(self.hbitmap,
-                  proportion=0,
-                  flag=wx.ALIGN_CENTER_VERTICAL |
-                  wx.ALIGN_CENTER_HORIZONTAL |
-                  wx.ALL,
-                  border=3)  # image
-        sizer.Add(gisdbase_boxsizer, proportion=0,
-                  flag=wx.RIGHT | wx.LEFT | wx.TOP | wx.EXPAND,
-                  border=3)  # GISDBASE setting
-
-        # warning/error message
-        sizer.Add(self.lmessage,
-                  proportion=0,
-                  flag=wx.ALIGN_LEFT | wx.ALL | wx.EXPAND, border=5)
-        sizer.Add(location_mapset_sizer, proportion=1,
-                  flag=wx.RIGHT | wx.LEFT | wx.EXPAND,
-                  border=1)
-        sizer.Add(btns_sizer, proportion=0,
-                  flag=wx.ALIGN_CENTER_VERTICAL |
-                  wx.ALIGN_CENTER_HORIZONTAL |
-                  wx.RIGHT | wx.LEFT,
-                  border=3)
-
-        self.panel.SetAutoLayout(True)
-        self.panel.SetSizer(sizer)
-        sizer.Fit(self.panel)
-        sizer.SetSizeHints(self)
-        self.Layout()
-
-    def _showWarning(self, text):
-        """Displays a warning, hint or info message to the user.
-
-        This function can be used for all kinds of messages except for
-        error messages.
-
-        .. note::
-            There is no cleaning procedure. You should call _hideMessage when
-            you know that there is everything correct now.
-        """
-        self.lmessage.SetLabel(text)
-        self.sizer.Layout()
-
-    def _showError(self, text):
-        """Displays a error message to the user.
-
-        This function should be used only when something serious and unexpected
-        happens, otherwise _showWarning should be used.
-
-        .. note::
-            There is no cleaning procedure. You should call _hideMessage when
-            you know that there is everything correct now.
-        """
-        self.lmessage.SetLabel(_("Error: {text}").format(text=text))
-        self.sizer.Layout()
-
-    def _hideMessage(self):
-        """Clears/hides the error message."""
-        # we do no hide widget
-        # because we do not want the dialog to change the size
-        self.lmessage.SetLabel("")
-        self.sizer.Layout()
-
-    def GetRCValue(self, value):
-        """Return GRASS variable (read from GISRC)
-        """
-        if value in self.grassrc:
-            return self.grassrc[value]
-        else:
-            return None
-
-    def SuggestDatabase(self):
-        """Suggest (set) possible GRASS Database value"""
-        # only if nothing is set (<UNKNOWN> comes from init script)
-        if self.GetRCValue("LOCATION_NAME") != "<UNKNOWN>":
-            return
-        path = get_possible_database_path()
-        if path:
-            try:
-                self.tgisdbase.SetValue(path)
-            except UnicodeDecodeError:
-                # restore previous state
-                # wizard gives error in this case, we just ignore
-                path = None
-                self.tgisdbase.SetValue(self.gisdbase)
-            # if we still have path
-            if path:
-                self.gisdbase = path
-                self.OnSetDatabase(None)
-        else:
-            # nothing found
-            # TODO: should it be warning, hint or message?
-            self._showWarning(_(
-                'GRASS needs a directory (GRASS database) '
-                'in which to store its data. '
-                'Create one now if you have not already done so. '
-                'A popular choice is "grassdata", located in '
-                'your home directory. '
-                'Press Browse button to select the directory.'))
-
-    def OnCreateLocation(self, event):
-        """Location wizard started"""
-        grassdatabase, location, mapset = (
-            create_location_interactively(self, self.gisdbase)
-        )
-        if location is not None:
-            self.OnSelectLocation(None)
-            self.lbmapsets.SetSelection(self.listOfMapsets.index(mapset))
-            self.bstart.SetFocus()
-            self.tgisdbase.SetValue(grassdatabase)
-            self.OnSetDatabase(None)
-            self.UpdateMapsets(os.path.join(grassdatabase, location))
-            self.lblocations.SetSelection(
-                self.listOfLocations.index(location))
-            self.lbmapsets.SetSelection(0)
-            self.SetLocation(grassdatabase, location, mapset)
-
-    # the event can be refactored out by using lambda in bind
-    def OnRenameMapset(self, event):
-        """Rename selected mapset
-        """
-        location = self.listOfLocations[self.lblocations.GetSelection()]
-        mapset = self.listOfMapsets[self.lbmapsets.GetSelection()]
-        try:
-            newmapset = rename_mapset_interactively(self, self.gisdbase,
-                                                    location, mapset)
-            if newmapset:
-                self.OnSelectLocation(None)
-                self.lbmapsets.SetSelection(
-                    self.listOfMapsets.index(newmapset))
-        except Exception as e:
-            GError(parent=self,
-                   message=_("Unable to rename mapset: %s") % e,
-                   showTraceback=False)
-
-    def OnRenameLocation(self, event):
-        """Rename selected location
-        """
-        location = self.listOfLocations[self.lblocations.GetSelection()]
-        try:
-            newlocation = rename_location_interactively(self, self.gisdbase,
-                                                        location)
-            if newlocation:
-                self.UpdateLocations(self.gisdbase)
-                self.lblocations.SetSelection(
-                    self.listOfLocations.index(newlocation))
-                self.UpdateMapsets(newlocation)
-        except Exception as e:
-            GError(parent=self,
-                   message=_("Unable to rename location: %s") % e,
-                   showTraceback=False)
-
-    def OnDeleteMapset(self, event):
-        """
-        Delete selected mapset
-        """
-        location = self.listOfLocations[self.lblocations.GetSelection()]
-        mapset = self.listOfMapsets[self.lbmapsets.GetSelection()]
-        if (delete_mapset_interactively(self, self.gisdbase, location, mapset)):
-            self.OnSelectLocation(None)
-            self.lbmapsets.SetSelection(0)
-
-    def OnDeleteLocation(self, event):
-        """
-        Delete selected location
-        """
-        location = self.listOfLocations[self.lblocations.GetSelection()]
-        try:
-            if (delete_location_interactively(self, self.gisdbase, location)):
-                self.UpdateLocations(self.gisdbase)
-                self.lblocations.SetSelection(0)
-                self.OnSelectLocation(None)
-                self.lbmapsets.SetSelection(0)
-        except Exception as e:
-            GError(parent=self,
-                   message=_("Unable to delete location: %s") % e,
-                   showTraceback=False)
-
-    def OnDownloadLocation(self, event):
-        """
-        Download location online
-        """
-        grassdatabase, location, mapset = download_location_interactively(
-            self, self.gisdbase
-        )
-        if location:
-            # get the new location to the list
-            self.UpdateLocations(grassdatabase)
-            # seems to be used in similar context
-            self.UpdateMapsets(os.path.join(grassdatabase, location))
-            self.lblocations.SetSelection(
-                self.listOfLocations.index(location))
-            # wizard does this as well, not sure if needed
-            self.SetLocation(grassdatabase, location, mapset)
-            # seems to be used in similar context
-            self.OnSelectLocation(None)
-
-    def UpdateLocations(self, dbase):
-        """Update list of locations"""
-        try:
-            self.listOfLocations = GetListOfLocations(dbase)
-        except (UnicodeEncodeError, UnicodeDecodeError) as e:
-            GError(parent=self,
-                   message=_("Unicode error detected. "
-                             "Check your locale settings. Details: {0}").format(e),
-                   showTraceback=False)
-
-        self.lblocations.Clear()
-        self.lblocations.InsertItems(self.listOfLocations, 0)
-
-        if len(self.listOfLocations) > 0:
-            self._hideMessage()
-            self.lblocations.SetSelection(0)
-        else:
-            self.lblocations.SetSelection(wx.NOT_FOUND)
-            self._showWarning(_("No GRASS Location found in '%s'."
-                                " Create a new Location or choose different"
-                                " GRASS database directory.")
-                              % self.gisdbase)
-
-        return self.listOfLocations
-
-    def UpdateMapsets(self, location):
-        """Update list of mapsets"""
-        self.FormerMapsetSelection = wx.NOT_FOUND  # for non-selectable item
-
-        self.listOfMapsetsSelectable = list()
-        self.listOfMapsets = GetListOfMapsets(self.gisdbase, location)
-
-        self.lbmapsets.Clear()
-
-        # disable mapset with denied permission
-        locationName = os.path.basename(location)
-
-        ret = RunCommand('g.mapset',
-                         read=True,
-                         flags='l',
-                         location=locationName,
-                         gisdbase=self.gisdbase)
-
-        if ret:
-            for line in ret.splitlines():
-                self.listOfMapsetsSelectable += line.split(' ')
-        else:
-            self.SetLocation(self.gisdbase, locationName, "PERMANENT")
-            # first run only
-            self.listOfMapsetsSelectable = copy.copy(self.listOfMapsets)
-
-        disabled = []
-        idx = 0
-        for mapset in self.listOfMapsets:
-            if mapset not in self.listOfMapsetsSelectable or \
-                    get_lockfile_if_present(self.gisdbase,
-                                            locationName, mapset):
-                disabled.append(idx)
-            idx += 1
-
-        self.lbmapsets.InsertItems(self.listOfMapsets, 0, disabled=disabled)
-
-        return self.listOfMapsets
-
-    def OnSelectLocation(self, event):
-        """Location selected"""
-        if event:
-            self.lblocations.SetSelection(event.GetIndex())
-
-        if self.lblocations.GetSelection() != wx.NOT_FOUND:
-            self.UpdateMapsets(
-                os.path.join(
-                    self.gisdbase,
-                    self.listOfLocations[
-                        self.lblocations.GetSelection()]))
-        else:
-            self.listOfMapsets = []
-
-        disabled = []
-        idx = 0
-        try:
-            locationName = self.listOfLocations[
-                self.lblocations.GetSelection()]
-        except IndexError:
-            locationName = ''
-
-        for mapset in self.listOfMapsets:
-            if mapset not in self.listOfMapsetsSelectable or \
-                    get_lockfile_if_present(self.gisdbase,
-                                            locationName, mapset):
-                disabled.append(idx)
-            idx += 1
-
-        self.lbmapsets.Clear()
-        self.lbmapsets.InsertItems(self.listOfMapsets, 0, disabled=disabled)
-
-        if len(self.listOfMapsets) > 0:
-            self.lbmapsets.SetSelection(0)
-            if locationName:
-                # enable start button when location and mapset is selected
-                self.bstart.Enable()
-                self.bstart.SetFocus()
-                self.bmapset.Enable()
-                # replacing disabled choice, perhaps just mapset needed
-                self.rename_location_button.Enable()
-                self.delete_location_button.Enable()
-                self.rename_mapset_button.Enable()
-                self.delete_mapset_button.Enable()
-        else:
-            self.lbmapsets.SetSelection(wx.NOT_FOUND)
-            self.bstart.Enable(False)
-            self.bmapset.Enable(False)
-            # this all was originally a choice, perhaps just mapset needed
-            self.rename_location_button.Enable(False)
-            self.delete_location_button.Enable(False)
-            self.rename_mapset_button.Enable(False)
-            self.delete_mapset_button.Enable(False)
-
-    def OnSelectMapset(self, event):
-        """Mapset selected"""
-        self.lbmapsets.SetSelection(event.GetIndex())
-
-        if event.GetText() not in self.listOfMapsetsSelectable:
-            self.lbmapsets.SetSelection(self.FormerMapsetSelection)
-        else:
-            self.FormerMapsetSelection = event.GetIndex()
-            event.Skip()
-
-    def OnSetDatabase(self, event):
-        """Database set"""
-        gisdbase = self.tgisdbase.GetValue()
-        self._hideMessage()
-        if not os.path.exists(gisdbase):
-            self._showError(_("Path '%s' doesn't exist.") % gisdbase)
-            return
-
-        self.gisdbase = self.tgisdbase.GetValue()
-        self.UpdateLocations(self.gisdbase)
-
-        self.OnSelectLocation(None)
-
-    def OnBrowse(self, event):
-        """'Browse' button clicked"""
-        if not event:
-            defaultPath = os.getenv('HOME')
-        else:
-            defaultPath = ""
-
-        dlg = wx.DirDialog(parent=self, message=_("Choose GIS Data Directory"),
-                           defaultPath=defaultPath, style=wx.DD_DEFAULT_STYLE)
-
-        if dlg.ShowModal() == wx.ID_OK:
-            self.gisdbase = dlg.GetPath()
-            self.tgisdbase.SetValue(self.gisdbase)
-            self.OnSetDatabase(event)
-
-        dlg.Destroy()
-
-    def OnCreateMapset(self, event):
-        """Create new mapset"""
-        gisdbase = self.tgisdbase.GetValue()
-        location = self.listOfLocations[self.lblocations.GetSelection()]
-        try:
-            mapset = create_mapset_interactively(self, gisdbase, location)
-            if mapset:
-                self.OnSelectLocation(None)
-                self.lbmapsets.SetSelection(self.listOfMapsets.index(mapset))
-                self.bstart.SetFocus()
-        except Exception as e:
-            GError(parent=self,
-                   message=_("Unable to create new mapset: %s") % e,
-                   showTraceback=False)
-
-    def OnStart(self, event):
-        """'Start GRASS' button clicked"""
-        dbase = self.tgisdbase.GetValue()
-        location = self.listOfLocations[self.lblocations.GetSelection()]
-        mapset = self.listOfMapsets[self.lbmapsets.GetSelection()]
-
-        lockfile = get_lockfile_if_present(dbase, location, mapset)
-        if lockfile:
-            dlg = wx.MessageDialog(
-                parent=self,
-                message=_(
-                    "GRASS is already running in selected mapset <%(mapset)s>\n"
-                    "(file %(lock)s found).\n\n"
-                    "Concurrent use not allowed.\n\n"
-                    "Do you want to try to remove .gislock (note that you "
-                    "need permission for this operation) and continue?") %
-                {'mapset': mapset, 'lock': lockfile},
-                caption=_("Lock file found"),
-                style=wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION | wx.CENTRE)
-
-            ret = dlg.ShowModal()
-            dlg.Destroy()
-            if ret == wx.ID_YES:
-                dlg1 = wx.MessageDialog(
-                    parent=self,
-                    message=_(
-                        "ARE YOU REALLY SURE?\n\n"
-                        "If you really are running another GRASS session doing this "
-                        "could corrupt your data. Have another look in the processor "
-                        "manager just to be sure..."),
-                    caption=_("Lock file found"),
-                    style=wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION | wx.CENTRE)
-
-                ret = dlg1.ShowModal()
-                dlg1.Destroy()
-
-                if ret == wx.ID_YES:
-                    try:
-                        os.remove(lockfile)
-                    except IOError as e:
-                        GError(_("Unable to remove '%(lock)s'.\n\n"
-                                 "Details: %(reason)s") % {'lock': lockfile, 'reason': e})
-                else:
-                    return
-            else:
-                return
-        self.SetLocation(dbase, location, mapset)
-        self.ExitSuccessfully()
-
-    def SetLocation(self, dbase, location, mapset):
-        SetSessionMapset(dbase, location, mapset)
-
-    def ExitSuccessfully(self):
-        self.Destroy()
-        sys.exit(self.exit_success)
-
-    def OnExit(self, event):
-        """'Exit' button clicked"""
-        self.Destroy()
-        sys.exit(self.exit_user_requested)
-
-    def OnHelp(self, event):
-        """'Help' button clicked"""
-
-        # help text in lib/init/helptext.html
-        RunCommand('g.manual', entry='helptext')
-
-    def OnCloseWindow(self, event):
-        """Close window event"""
-        event.Skip()
-        sys.exit(self.exit_user_requested)
-
-
-class GListBox(ListCtrl, listmix.ListCtrlAutoWidthMixin):
-    """Use wx.ListCtrl instead of wx.ListBox, different style for
-    non-selectable items (e.g. mapsets with denied permission)"""
-
-    def __init__(self, parent, id, size,
-                 choices, disabled=[]):
-        ListCtrl.__init__(
-            self, parent, id, size=size, style=wx.LC_REPORT | wx.LC_NO_HEADER |
-            wx.LC_SINGLE_SEL | wx.BORDER_SUNKEN)
-
-        listmix.ListCtrlAutoWidthMixin.__init__(self)
-
-        self.InsertColumn(0, '')
-
-        self.selected = wx.NOT_FOUND
-
-        self._LoadData(choices, disabled)
-
-    def _LoadData(self, choices, disabled=[]):
-        """Load data into list
-
-        :param choices: list of item
-        :param disabled: list of indices of non-selectable items
-        """
-        idx = 0
-        count = self.GetItemCount()
-        for item in choices:
-            index = self.InsertItem(count + idx, item)
-            self.SetItem(index, 0, item)
-
-            if idx in disabled:
-                self.SetItemTextColour(idx, wx.Colour(150, 150, 150))
-            idx += 1
-
-    def Clear(self):
-        self.DeleteAllItems()
-
-    def InsertItems(self, choices, pos, disabled=[]):
-        self._LoadData(choices, disabled)
-
-    def SetSelection(self, item, force=False):
-        if item != wx.NOT_FOUND and \
-                (platform.system() != 'Windows' or force):
-            # Windows -> FIXME
-            self.SetItemState(
-                item,
-                wx.LIST_STATE_SELECTED,
-                wx.LIST_STATE_SELECTED)
-
-        self.selected = item
-
-    def GetSelection(self):
-        return self.selected
-
-
-class StartUp(wx.App):
-    """Start-up application"""
-
-    def OnInit(self):
-        StartUp = GRASSStartup()
-        StartUp.CenterOnScreen()
-        self.SetTopWindow(StartUp)
-        StartUp.Show()
-        StartUp.SuggestDatabase()
-
-        return 1
-
-
-if __name__ == "__main__":
-    if os.getenv("GISBASE") is None:
-        sys.exit("Failed to start GUI, GRASS GIS is not running.")
-
-    GRASSStartUp = StartUp(0)
-    GRASSStartUp.MainLoop()

+ 0 - 41
gui/wxpython/gis_set_error.py

@@ -1,41 +0,0 @@
-"""
-@package gis_set_error
-
-GRASS start-up screen error message.
-
-(C) 2010-2011 by the GRASS Development Team
-
-This program is free software under the GNU General Public License
-(>=v2). Read the file COPYING that comes with GRASS for details.
-
-@author Martin Landa <landa.martin gmail.com>
-"""
-
-import sys
-
-# i18n is taken care of in the grass library code.
-# So we need to import it before any of the GUI code.
-# NOTE: in this particular case, we don't really need the grass library;
-# NOTE: we import it just for the side effects of gettext.install()
-
-import wx
-
-
-def main():
-    app = wx.App()
-
-    if len(sys.argv) == 1:
-        msg = "Unknown reason"
-    else:
-        msg = ''
-        for m in sys.argv[1:]:
-            msg += m
-
-    wx.MessageBox(caption="Error",
-                  message=msg,
-                  style=wx.OK | wx.ICON_ERROR)
-
-    app.MainLoop()
-
-if __name__ == "__main__":
-    main()

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

@@ -37,9 +37,6 @@ if os.path.join(globalvar.ETCDIR, "python") not in sys.path:
 
 
 from grass.script import core as grass
 from grass.script import core as grass
 from grass.script.utils import decode
 from grass.script.utils import decode
-from startup.guiutils import (
-    can_switch_mapset_interactive
-)
 
 
 from core.gcmd import RunCommand, GError, GMessage
 from core.gcmd import RunCommand, GError, GMessage
 from core.settings import UserSettings, GetDisplayVectSettings
 from core.settings import UserSettings, GetDisplayVectSettings
@@ -63,12 +60,13 @@ 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 grass.grassdb.checks import is_current_mapset_in_demolocation
 from startup.guiutils import (
 from startup.guiutils import (
+    can_switch_mapset_interactive,
     switch_mapset_interactively,
     switch_mapset_interactively,
     create_mapset_interactively,
     create_mapset_interactively,
     create_location_interactively
     create_location_interactively
 )
 )
+from grass.grassdb.checks import is_first_time_user
 
 
 
 
 class GMFrame(wx.Frame):
 class GMFrame(wx.Frame):
@@ -446,7 +444,7 @@ class GMFrame(wx.Frame):
                 lchecked=True,
                 lchecked=True,
                 lcmd=["d.vect", "map={}".format(layer_name)],
                 lcmd=["d.vect", "map={}".format(layer_name)],
             )
             )
-        if is_current_mapset_in_demolocation():
+        if is_first_time_user():
             # Show only after everything is initialized for proper map alignment.
             # Show only after everything is initialized for proper map alignment.
             wx.CallLater(1000, show_demo)
             wx.CallLater(1000, show_demo)
 
 

+ 22 - 63
gui/wxpython/startup/guiutils.py

@@ -16,7 +16,6 @@ This is for code which depend on something from GUI (wx or wxGUI).
 
 
 
 
 import os
 import os
-import sys
 import wx
 import wx
 
 
 from grass.grassdb.checks import (
 from grass.grassdb.checks import (
@@ -30,8 +29,10 @@ from grass.grassdb.checks import (
     get_reasons_mapsets_not_removable,
     get_reasons_mapsets_not_removable,
     get_reasons_location_not_removable,
     get_reasons_location_not_removable,
     get_reasons_locations_not_removable,
     get_reasons_locations_not_removable,
-    get_reasons_grassdb_not_removable
+    get_reasons_grassdb_not_removable,
+    is_fallback_session
 )
 )
+import grass.grassdb.config as cfg
 
 
 from grass.grassdb.create import create_mapset, get_default_mapset_name
 from grass.grassdb.create import create_mapset, get_default_mapset_name
 from grass.grassdb.manage import (
 from grass.grassdb.manage import (
@@ -43,21 +44,14 @@ from grass.grassdb.manage import (
 )
 )
 from grass.script.core import create_environment
 from grass.script.core import create_environment
 from grass.script.utils import try_remove
 from grass.script.utils import try_remove
+from grass.script import gisenv
 
 
-from core import globalvar
-from core.gcmd import GError, GMessage, DecodeString, RunCommand
+from core.gcmd import GError, GMessage, RunCommand
 from gui_core.dialogs import TextEntryDialog
 from gui_core.dialogs import TextEntryDialog
 from location_wizard.dialogs import RegionDef
 from location_wizard.dialogs import RegionDef
 from gui_core.widgets import GenericValidator
 from gui_core.widgets import GenericValidator
 
 
 
 
-def SetSessionMapset(database, location, mapset):
-    """Sets database, location and mapset for the current session"""
-    RunCommand("g.gisenv", set="GISDBASE=%s" % database)
-    RunCommand("g.gisenv", set="LOCATION_NAME=%s" % location)
-    RunCommand("g.gisenv", set="MAPSET=%s" % mapset)
-
-
 class MapsetDialog(TextEntryDialog):
 class MapsetDialog(TextEntryDialog):
     def __init__(self, parent=None, default=None, message=None, caption=None,
     def __init__(self, parent=None, default=None, message=None, caption=None,
                  database=None, location=None):
                  database=None, location=None):
@@ -112,57 +106,6 @@ class LocationDialog(TextEntryDialog):
         return is_location_name_valid(self.database, text)
         return is_location_name_valid(self.database, text)
 
 
 
 
-# TODO: similar to (but not the same as) read_gisrc function in grass.py
-def read_gisrc():
-    """Read variables from a current GISRC file
-
-    Returns a dictionary representation of the file content.
-    """
-    grassrc = {}
-
-    gisrc = os.getenv("GISRC")
-
-    if gisrc and os.path.isfile(gisrc):
-        try:
-            rc = open(gisrc, "r")
-            for line in rc.readlines():
-                try:
-                    key, val = line.split(":", 1)
-                except ValueError as e:
-                    sys.stderr.write(
-                        _('Invalid line in GISRC file (%s):%s\n' % (e, line)))
-                grassrc[key.strip()] = DecodeString(val.strip())
-        finally:
-            rc.close()
-
-    return grassrc
-
-
-def GetVersion():
-    """Gets version and revision
-
-    Returns tuple `(version, revision)`. For standard releases revision
-    is an empty string.
-
-    Revision string is currently wrapped in parentheses with added
-    leading space. This is an implementation detail and legacy and may
-    change anytime.
-    """
-    versionFile = open(os.path.join(globalvar.ETCDIR, "VERSIONNUMBER"))
-    versionLine = versionFile.readline().rstrip('\n')
-    versionFile.close()
-    try:
-        grassVersion, grassRevision = versionLine.split(' ', 1)
-        if grassVersion.endswith('dev'):
-            grassRevisionStr = ' (%s)' % grassRevision
-        else:
-            grassRevisionStr = ''
-    except ValueError:
-        grassVersion = versionLine
-        grassRevisionStr = ''
-    return (grassVersion, grassRevisionStr)
-
-
 def create_mapset_interactively(guiparent, grassdb, location):
 def create_mapset_interactively(guiparent, grassdb, location):
     """
     """
     Create new mapset
     Create new mapset
@@ -717,6 +660,9 @@ def import_file(guiparent, filePath, env):
 def switch_mapset_interactively(guiparent, giface, dbase, location, mapset,
 def switch_mapset_interactively(guiparent, giface, dbase, location, mapset,
                                 show_confirmation=False):
                                 show_confirmation=False):
     """Switch current mapset. Emits giface.currentMapsetChanged signal."""
     """Switch current mapset. Emits giface.currentMapsetChanged signal."""
+    # Decide if a user is in a fallback session
+    fallback_session = is_fallback_session()
+
     if dbase:
     if dbase:
         if RunCommand('g.mapset', parent=guiparent,
         if RunCommand('g.mapset', parent=guiparent,
                       location=location,
                       location=location,
@@ -751,4 +697,17 @@ def switch_mapset_interactively(guiparent, giface, dbase, location, mapset,
             if show_confirmation:
             if show_confirmation:
                 GMessage(parent=guiparent,
                 GMessage(parent=guiparent,
                          message=_("Current mapset is <%s>.") % mapset)
                          message=_("Current mapset is <%s>.") % mapset)
-            giface.currentMapsetChanged.emit(dbase=None, location=None, mapset=mapset)
+            giface.currentMapsetChanged.emit(dbase=None,
+                                             location=None,
+                                             mapset=mapset)
+
+    if fallback_session:
+        tmp_dbase = os.environ["TMPDIR"]
+        tmp_loc = cfg.temporary_location
+        if tmp_dbase != gisenv()["GISDBASE"]:
+            # Delete temporary location
+            delete_location(tmp_dbase, tmp_loc)
+            # Remove useless temporary grassdb node
+            giface.grassdbChanged.emit(
+                location=location, grassdb=tmp_dbase, action="delete", element="grassdb"
+            )

+ 119 - 129
lib/init/grass.py

@@ -586,6 +586,21 @@ def read_gisrc(filename):
     return kv
     return kv
 
 
 
 
+def write_gisrcrc(gisrcrc, gisrc, skip_variable=None):
+    """Reads gisrc file and write to gisrcrc"""
+    debug("Reading %s" % gisrc)
+    number = 0
+    with open(gisrc, "r") as f:
+        lines = f.readlines()
+        for line in lines:
+            if skip_variable in line:
+                del lines[number]
+            number += 1
+    with open(gisrcrc, "w") as f:
+        for line in lines:
+            f.write(line)
+
+
 def read_env_file(path):
 def read_env_file(path):
     kv = {}
     kv = {}
     f = open(path, "r")
     f = open(path, "r")
@@ -605,7 +620,7 @@ def write_gisrc(kv, filename, append=False):
     f.close()
     f.close()
 
 
 
 
-def set_mapset_to_gisrc(gisrc, grassdb, location, mapset):
+def add_mapset_to_gisrc(gisrc, grassdb, location, mapset):
     if os.access(gisrc, os.R_OK):
     if os.access(gisrc, os.R_OK):
         kv = read_gisrc(gisrc)
         kv = read_gisrc(gisrc)
     else:
     else:
@@ -616,6 +631,27 @@ def set_mapset_to_gisrc(gisrc, grassdb, location, mapset):
     write_gisrc(kv, gisrc)
     write_gisrc(kv, gisrc)
 
 
 
 
+def add_last_mapset_to_gisrc(gisrc, last_mapset_path):
+    if os.access(gisrc, os.R_OK):
+        kv = read_gisrc(gisrc)
+    else:
+        kv = {}
+    kv["LAST_MAPSET_PATH"] = last_mapset_path
+    write_gisrc(kv, gisrc)
+
+
+def create_fallback_session(gisrc, tmpdir):
+    """Creates fallback temporary session"""
+    # Create temporary location
+    set_mapset(
+        gisrc=gisrc,
+        geofile="XY",
+        create_new=True,
+        tmp_location=True,
+        tmpdir=tmpdir,
+    )
+
+
 def read_gui(gisrc, default_gui):
 def read_gui(gisrc, default_gui):
     grass_gui = None
     grass_gui = None
     # At this point the GRASS user interface variable has been set from the
     # At this point the GRASS user interface variable has been set from the
@@ -1165,7 +1201,7 @@ def set_mapset(
                         )
                         )
                     )
                     )
                     writefile(os.path.join(path, "WIND"), s)
                     writefile(os.path.join(path, "WIND"), s)
-        set_mapset_to_gisrc(gisrc, gisdbase, location_name, mapset)
+        add_mapset_to_gisrc(gisrc, gisdbase, location_name, mapset)
     else:
     else:
         fatal(
         fatal(
             _(
             _(
@@ -1176,77 +1212,6 @@ def set_mapset(
         )
         )
 
 
 
 
-def set_mapset_interactive(grass_gui):
-    """User selects Location and Mapset in an interative way
-
-    The gisrc (GRASS environment file) is written at the end.
-    """
-    if not os.path.exists(wxpath("gis_set.py")) and grass_gui != "text":
-        debug("No GUI available, switching to text mode")
-        return False
-
-    # Check for text interface
-    if grass_gui == "text":
-        # TODO: maybe this should be removed and solved from outside
-        # this depends on what we expect from this function
-        # should gisrc be ok after running or is it allowed to be still not set
-        pass
-    # Check for GUI
-    elif grass_gui in ("gtext", "wxpython"):
-        gui_startup(grass_gui)
-    else:
-        # Shouldn't need this but you never know
-        fatal(
-            _(
-                "Invalid user interface specified - <%s>. "
-                "Use the --help option to see valid interface names."
-            )
-            % grass_gui
-        )
-
-    return True
-
-
-def gui_startup(grass_gui):
-    """Start GUI for startup (setting gisrc file)"""
-    if grass_gui in ("wxpython", "gtext"):
-        ret = call([os.getenv("GRASS_PYTHON"), wxpath("gis_set.py")])
-
-    # this if could be simplified to three branches (0, 5, rest)
-    # if there is no need to handle unknown code separately
-    if ret == 0:
-        pass
-    elif ret in [1, 2]:
-        # 1 probably error coming from gis_set.py
-        # 2 probably file not found from python interpreter
-        # formerly we were starting in text mode instead, now we just fail
-        # which is more straightforward for everybody
-        fatal(
-            _(
-                "Error in GUI startup. See messages above (if any)"
-                " and if necessary, please"
-                " report this error to the GRASS developers.\n"
-                "On systems with package manager, make sure you have the right"
-                " GUI package, probably named grass-gui, installed.\n"
-                "To run GRASS GIS in text mode use the --text flag.\n"
-                "Use '--help' for further options\n"
-                "     {cmd_name} --help\n"
-                "See also: https://grass.osgeo.org/{cmd_name}/manuals/helptext.html"
-            ).format(cmd_name=CMD_NAME)
-        )
-    elif ret == 5:  # defined in gui/wxpython/gis_set.py
-        # User wants to exit from GRASS
-        message(_("Exit was requested in GUI.\nGRASS GIS will not start. Bye."))
-        sys.exit(0)
-    else:
-        fatal(
-            _(
-                "Invalid return code from GUI startup script.\n"
-                "Please advise GRASS developers of this error."
-            )
-        )
-
-
 # we don't follow the LOCATION_NAME legacy naming here but we have to still
 # we don't follow the LOCATION_NAME legacy naming here but we have to still
 # translate to it, so always double check
 # translate to it, so always double check
 class MapsetSettings(object):
 class MapsetSettings(object):
@@ -1276,6 +1241,18 @@ class MapsetSettings(object):
         return self.gisdbase and self.location and self.mapset
         return self.gisdbase and self.location and self.mapset
 
 
 
 
+def get_mapset_settings(gisrc):
+    """Get the settings of Location and Mapset from the gisrc file"""
+    mapset_settings = MapsetSettings()
+    kv = read_gisrc(gisrc)
+    mapset_settings.gisdbase = kv.get("GISDBASE")
+    mapset_settings.location = kv.get("LOCATION_NAME")
+    mapset_settings.mapset = kv.get("MAPSET")
+    if not mapset_settings.is_valid():
+        return None
+    return mapset_settings
+
+
 # TODO: does it really makes sense to tell user about gisrcrc?
 # TODO: does it really makes sense to tell user about gisrcrc?
 # anything could have happened in between loading from gisrcrc and now
 # anything could have happened in between loading from gisrcrc and now
 # (we do e.g. GUI or creating loctation)
 # (we do e.g. GUI or creating loctation)
@@ -1284,12 +1261,8 @@ def load_gisrc(gisrc, gisrcrc):
 
 
     :returns: MapsetSettings object
     :returns: MapsetSettings object
     """
     """
-    mapset_settings = MapsetSettings()
-    kv = read_gisrc(gisrc)
-    mapset_settings.gisdbase = kv.get("GISDBASE")
-    mapset_settings.location = kv.get("LOCATION_NAME")
-    mapset_settings.mapset = kv.get("MAPSET")
-    if not mapset_settings.is_valid():
+    mapset_settings = get_mapset_settings(gisrc)
+    if not mapset_settings:
         fatal(
         fatal(
             _(
             _(
                 "Error reading data path information from g.gisenv.\n"
                 "Error reading data path information from g.gisenv.\n"
@@ -1307,22 +1280,6 @@ def load_gisrc(gisrc, gisrcrc):
     return mapset_settings
     return mapset_settings
 
 
 
 
-def can_start_in_gisrc_mapset(gisrc, ignore_lock=False):
-    """Check if a mapset from a gisrc file is usable for a new session"""
-    from grass.grassdb.checks import can_start_in_mapset
-
-    mapset_settings = MapsetSettings()
-    kv = read_gisrc(gisrc)
-    mapset_settings.gisdbase = kv.get("GISDBASE")
-    mapset_settings.location = kv.get("LOCATION_NAME")
-    mapset_settings.mapset = kv.get("MAPSET")
-    if not mapset_settings.is_valid():
-        return False
-    return can_start_in_mapset(
-        mapset_path=mapset_settings.full_mapset, ignore_lock=ignore_lock
-    )
-
-
 # load environmental variables from grass_env_file
 # load environmental variables from grass_env_file
 def load_env(grass_env_file):
 def load_env(grass_env_file):
     if not os.access(grass_env_file, os.R_OK):
     if not os.access(grass_env_file, os.R_OK):
@@ -1606,6 +1563,8 @@ def lock_mapset(mapset_path, force_gislock_removal, user):
     Behavior on error must be changed somehow; now it fatals but GUI case is
     Behavior on error must be changed somehow; now it fatals but GUI case is
     unresolved.
     unresolved.
     """
     """
+    from grass.grassdb.checks import is_mapset_valid
+
     if not os.path.exists(mapset_path):
     if not os.path.exists(mapset_path):
         fatal(_("Path '%s' doesn't exist") % mapset_path)
         fatal(_("Path '%s' doesn't exist") % mapset_path)
     if not os.access(mapset_path, os.W_OK):
     if not os.access(mapset_path, os.W_OK):
@@ -2555,39 +2514,71 @@ def main():
         save_gui(gisrc, grass_gui)
         save_gui(gisrc, grass_gui)
 
 
     # Parsing argument to get LOCATION
     # Parsing argument to get LOCATION
+    # Mapset is not specified in command line arguments
     if not params.mapset and not params.tmp_location:
     if not params.mapset and not params.tmp_location:
-        # Mapset is not specified in command line arguments.
-        last_mapset_usable = can_start_in_gisrc_mapset(
-            gisrc=gisrc, ignore_lock=params.force_gislock_removal
+        # Get mapset parameters from gisrc file
+        mapset_settings = get_mapset_settings(gisrc)
+        last_mapset_path = mapset_settings.full_mapset
+        # Check if mapset from gisrc is usable
+        from grass.grassdb.checks import can_start_in_mapset
+
+        last_mapset_usable = can_start_in_mapset(
+            mapset_path=last_mapset_path,
+            ignore_lock=params.force_gislock_removal,
         )
         )
         debug(f"last_mapset_usable: {last_mapset_usable}")
         debug(f"last_mapset_usable: {last_mapset_usable}")
         if not last_mapset_usable:
         if not last_mapset_usable:
-            import grass.app as ga
-            from grass.grassdb.checks import can_start_in_mapset
-
-            # Try to use demolocation
-            grassdb, location, mapset = ga.ensure_demolocation()
-            demo_mapset_usable = can_start_in_mapset(
-                mapset_path=os.path.join(grassdb, location, mapset),
-                ignore_lock=params.force_gislock_removal,
-            )
-            debug(f"demo_mapset_usable: {demo_mapset_usable}")
-            if demo_mapset_usable:
-                set_mapset_to_gisrc(
-                    gisrc=gisrc, grassdb=grassdb, location=location, mapset=mapset
-                )
-            else:
-                # Try interactive startup
-                # User selects LOCATION and MAPSET if not set
-                if not set_mapset_interactive(grass_gui):
-                    # No GUI available, update gisrc file
+            from grass.app import ensure_default_data_hierarchy
+            from grass.grassdb.checks import is_first_time_user
+
+            fallback_session = False
+
+            # Add last used mapset to gisrc
+            add_last_mapset_to_gisrc(gisrc, last_mapset_path)
+
+            if is_first_time_user():
+                # Ensure default data hierarchy
+                (
+                    default_gisdbase,
+                    default_location,
+                    unused_default_mapset,
+                    default_mapset_path,
+                ) = ensure_default_data_hierarchy()
+
+                if not default_gisdbase:
                     fatal(
                     fatal(
                         _(
                         _(
-                            "<{0}> requested, but not available. Run GRASS in text "
-                            "mode (--text) or install missing package (usually "
-                            "'grass-gui')."
-                        ).format(grass_gui)
+                            "Failed to start GRASS GIS, grassdata directory could not be found or created."
+                        )
                     )
                     )
+                elif not default_location:
+                    fatal(
+                        _(
+                            "Failed to start GRASS GIS, no default location to copy in the installation or copying failed."
+                        )
+                    )
+                if can_start_in_mapset(
+                    mapset_path=default_mapset_path, ignore_lock=False
+                ):
+                    # Use the default location/mapset.
+                    set_mapset(gisrc=gisrc, arg=default_mapset_path)
+                else:
+                    fallback_session = True
+                    add_last_mapset_to_gisrc(gisrc, default_mapset_path)
+            else:
+                fallback_session = True
+
+            if fallback_session:
+                if grass_gui == "text":
+                    # Fallback in command line is just failing in a standard way.
+                    set_mapset(gisrc=gisrc, arg=last_mapset_path)
+                else:
+                    # Create fallback temporary session
+                    create_fallback_session(gisrc, tmpdir)
+                    params.tmp_location = True
+        else:
+            # Use the last used mapset.
+            set_mapset(gisrc=gisrc, arg=last_mapset_path)
     else:
     else:
         # Mapset was specified in command line parameters.
         # Mapset was specified in command line parameters.
         if params.tmp_location:
         if params.tmp_location:
@@ -2627,12 +2618,8 @@ def main():
             force_gislock_removal=params.force_gislock_removal,
             force_gislock_removal=params.force_gislock_removal,
         )
         )
     except Exception as e:
     except Exception as e:
-        msg = e.args[0]
-        if grass_gui == "wxpython":
-            call([os.getenv("GRASS_PYTHON"), wxpath("gis_set_error.py"), msg])
-            sys.exit(_("Exiting..."))
-        else:
-            fatal(msg)
+        fatal(e.args[0])
+        sys.exit(_("Exiting..."))
 
 
     # unlock the mapset which is current at the time of turning off
     # unlock the mapset which is current at the time of turning off
     # in case mapset was changed
     # in case mapset was changed
@@ -2714,8 +2701,11 @@ def main():
 
 
         # here we are at the end of grass session
         # here we are at the end of grass session
         clean_all()
         clean_all()
-        if not params.tmp_location:
-            writefile(gisrcrc, readfile(gisrc))
+        mapset_settings = load_gisrc(gisrc, gisrcrc=gisrcrc)
+        if not params.tmp_location or (
+            params.tmp_location and mapset_settings.gisdbase != os.environ["TMPDIR"]
+        ):
+            write_gisrcrc(gisrcrc, gisrc, skip_variable="LAST_MAPSET_PATH")
         # After this point no more grass modules may be called
         # After this point no more grass modules may be called
         # done message at last: no atexit.register()
         # done message at last: no atexit.register()
         # or register done_message()
         # or register done_message()

+ 21 - 13
python/grass/app/data.py

@@ -17,6 +17,7 @@ import tempfile
 import getpass
 import getpass
 import sys
 import sys
 from shutil import copytree, ignore_patterns
 from shutil import copytree, ignore_patterns
+import grass.grassdb.config as cfg
 
 
 from grass.grassdb.checks import is_location_valid
 from grass.grassdb.checks import is_location_valid
 
 
@@ -141,18 +142,25 @@ def create_startup_location_in_grassdb(grassdatabase, startup_location_name):
     return False
     return False
 
 
 
 
-def ensure_demolocation():
-    """Ensure that demolocation exists
+def ensure_default_data_hierarchy():
+    """Ensure that default gisdbase, location and mapset exist.
+    Creates database directory based on the default path determined
+    according to OS if needed. Creates location if needed.
 
 
-    Creates both database directory and location if needed.
+    Returns the db, loc, mapset, mapset_path"""
 
 
-    Returns the db, location name, and preferred mapset of the demolocation.
-    """
-    grassdb = get_possible_database_path()
-    # If nothing found, try to create GRASS directory and copy startup loc
-    if grassdb is None:
-        grassdb = create_database_directory()
-    location = "world_latlong_wgs84"
-    if not is_location_valid(grassdb, location):
-        create_startup_location_in_grassdb(grassdb, location)
-    return (grassdb, location, "PERMANENT")
+    gisdbase = get_possible_database_path()
+    location = cfg.default_location
+    mapset = cfg.permanent_mapset
+
+    # If nothing found, try to create GRASS directory
+    if not gisdbase:
+        gisdbase = create_database_directory()
+
+    if not is_location_valid(gisdbase, location):
+        # If not valid, copy startup loc
+        create_startup_location_in_grassdb(gisdbase, location)
+
+    mapset_path = os.path.join(gisdbase, location, mapset)
+
+    return gisdbase, location, mapset, mapset_path

+ 1 - 1
python/grass/grassdb/Makefile

@@ -5,7 +5,7 @@ include $(MODULE_TOPDIR)/include/Make/Python.make
 
 
 DSTDIR = $(ETC)/python/grass/grassdb
 DSTDIR = $(ETC)/python/grass/grassdb
 
 
-MODULES = checks create data manage
+MODULES = checks create data manage config
 
 
 PYFILES := $(patsubst %,$(DSTDIR)/%.py,$(MODULES) __init__)
 PYFILES := $(patsubst %,$(DSTDIR)/%.py,$(MODULES) __init__)
 PYCFILES := $(patsubst %,$(DSTDIR)/%.pyc,$(MODULES) __init__)
 PYCFILES := $(patsubst %,$(DSTDIR)/%.pyc,$(MODULES) __init__)

+ 50 - 3
python/grass/grassdb/checks.py

@@ -9,7 +9,6 @@ for details.
 .. sectionauthor:: Vaclav Petras <wenzeslaus gmail com>
 .. sectionauthor:: Vaclav Petras <wenzeslaus gmail com>
 """
 """
 
 
-
 import os
 import os
 import sys
 import sys
 import datetime
 import datetime
@@ -18,6 +17,8 @@ from grass.script import gisenv
 import grass.script as gs
 import grass.script as gs
 import glob
 import glob
 
 
+import grass.grassdb.config as cfg
+
 
 
 def mapset_exists(database, location, mapset):
 def mapset_exists(database, location, mapset):
     """Returns True whether mapset path exists."""
     """Returns True whether mapset path exists."""
@@ -115,8 +116,33 @@ def get_mapset_owner(mapset_path):
         return None
         return None
 
 
 
 
-def is_current_mapset_in_demolocation():
-    return gisenv()["LOCATION_NAME"] == "world_latlong_wgs84"
+def is_fallback_session():
+    """Checks if a user encounters a fallback GRASS session.
+
+    Returns True if a user encounters a fallback session.
+    It occurs when a last mapset is not usable and at the same time
+    a user is in a temporary location.
+    """
+    if "LAST_MAPSET_PATH" in gisenv().keys():
+        return is_mapset_current(
+            os.environ["TMPDIR"], cfg.temporary_location, cfg.permanent_mapset
+        )
+    return False
+
+
+def is_first_time_user():
+    """Check if a user is a first-time user.
+
+    Returns True if a user is a first-time user.
+    It occurs when a gisrc file has initial settings either in last used mapset
+    or in current mapset settings.
+    """
+    genv = gisenv()
+    if "LAST_MAPSET_PATH" in genv.keys():
+        return genv["LAST_MAPSET_PATH"] == os.path.join(
+            os.getcwd(), cfg.unknown_location, cfg.unknown_mapset
+        )
+    return False
 
 
 
 
 def is_mapset_locked(mapset_path):
 def is_mapset_locked(mapset_path):
@@ -169,6 +195,27 @@ def can_start_in_mapset(mapset_path, ignore_lock=False):
     return True
     return True
 
 
 
 
+def get_reason_id_mapset_not_usable(mapset_path):
+    """It finds a reason why mapset is not usable.
+
+    Returns a reason id as a string.
+    If mapset path is None or no reason found, returns None.
+    """
+    # Check whether mapset exists
+    if not os.path.exists(mapset_path):
+        return "non-existent"
+    # Check whether mapset is valid
+    elif not is_mapset_valid(mapset_path):
+        return "invalid"
+    # Check whether mapset is owned by current user
+    elif not is_current_user_mapset_owner(mapset_path):
+        return "different-owner"
+    # Check whether mapset is locked
+    elif is_mapset_locked(mapset_path):
+        return "locked"
+    return None
+
+
 def dir_contains_location(path):
 def dir_contains_location(path):
     """Return True if directory *path* contains a valid location"""
     """Return True if directory *path* contains a valid location"""
     if not os.path.isdir(path):
     if not os.path.isdir(path):

+ 17 - 0
python/grass/grassdb/config.py

@@ -0,0 +1,17 @@
+"""
+Set global variables for objects in a GRASS GIS Spatial Database
+
+(C) 2020 by the GRASS Development Team
+This program is free software under the GNU General Public
+License (>=v2). Read the file COPYING that comes with GRASS
+for details.
+
+.. sectionauthor:: Linda Kladivova <linda.kladivova seznam cz>
+"""
+
+# global variables
+default_location = "world_latlong_wgs84"
+temporary_location = "tmploc"
+unknown_location = "<UNKNOWN>"
+unknown_mapset = "<UNKNOWN>"
+permanent_mapset = "PERMANENT"

+ 7 - 0
python/grass/grassdb/manage.py

@@ -46,3 +46,10 @@ def rename_mapset(database, location, old_name, new_name):
 def rename_location(database, old_name, new_name):
 def rename_location(database, old_name, new_name):
     """Rename location from *old_name* to *new_name*"""
     """Rename location from *old_name* to *new_name*"""
     os.rename(os.path.join(database, old_name), os.path.join(database, new_name))
     os.rename(os.path.join(database, old_name), os.path.join(database, new_name))
+
+
+def split_mapset_path(mapset_path):
+    """Split mapset path to three parts - grassdb, location, mapset"""
+    path, mapset = os.path.split(mapset_path.rstrip(os.sep))
+    grassdb, location = os.path.split(path)
+    return grassdb, location, mapset