From 8fe13636b7081332b09d5b75faa2dac3fa015bef Mon Sep 17 00:00:00 2001 From: Linda Kladivova <49241681+lindakladivova@users.noreply.github.com> Date: Mon, 12 Apr 2021 23:45:35 -0500 Subject: [PATCH] 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 ``. * 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`. --- gui/wxpython/Makefile | 2 +- gui/wxpython/datacatalog/catalog.py | 43 +- gui/wxpython/datacatalog/infomanager.py | 59 +- gui/wxpython/datacatalog/tree.py | 18 +- gui/wxpython/gis_set.py | 955 ------------------------ gui/wxpython/gis_set_error.py | 41 - gui/wxpython/lmgr/frame.py | 8 +- gui/wxpython/startup/guiutils.py | 85 +-- lib/init/grass.py | 248 +++--- python/grass/app/data.py | 34 +- python/grass/grassdb/Makefile | 2 +- python/grass/grassdb/checks.py | 53 +- python/grass/grassdb/config.py | 17 + python/grass/grassdb/manage.py | 7 + 14 files changed, 344 insertions(+), 1228 deletions(-) delete mode 100644 gui/wxpython/gis_set.py delete mode 100644 gui/wxpython/gis_set_error.py create mode 100644 python/grass/grassdb/config.py diff --git a/gui/wxpython/Makefile b/gui/wxpython/Makefile index 97d7d45d2c6..251c3a70159 100644 --- a/gui/wxpython/Makefile +++ b/gui/wxpython/Makefile @@ -14,7 +14,7 @@ SRCFILES := $(wildcard icons/*.py scripts/*.py xml/*) \ mapswipe/*.py modules/*.py nviz/*.py psmap/*.py rdigit/*.py \ rlisetup/*.py startup/*.py timeline/*.py vdigit/*.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)) \ $(patsubst %.py,$(DSTDIR)/%.pyc,$(filter %.py,$(SRCFILES))) diff --git a/gui/wxpython/datacatalog/catalog.py b/gui/wxpython/datacatalog/catalog.py index 1248ace86f0..ab85860f913 100644 --- a/gui/wxpython/datacatalog/catalog.py +++ b/gui/wxpython/datacatalog/catalog.py @@ -26,10 +26,14 @@ from datacatalog.infomanager import DataCatalogInfoManager from gui_core.wrap import Menu from gui_core.forms import GUI +from grass.script import gisenv 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): @@ -55,7 +59,9 @@ def __init__(self, parent, giface=None, id=wx.ID_ANY, self.tree.showNotification.connect(self.showNotification) # infobar for data catalog + delay = 2000 self.infoBar = InfoBar(self) + self.giface.currentMapsetChanged.connect(self.dismissInfobar) # infobar manager for data catalog self.infoManager = DataCatalogInfoManager(infobar=self.infoBar, @@ -65,9 +71,22 @@ def __init__(self, parent, giface=None, id=wx.ID_ANY, # some 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): """Do layout""" @@ -85,12 +104,22 @@ def _layout(self): def showDataStructureInfo(self): 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): self.infoManager.ShowImportDataInfo(self.OnImportOgrLayers, self.OnImportGdalLayers) def LoadItems(self): self.tree.ReloadTreeItems() + def dismissInfobar(self): + if self.infoBar.IsShown(): + self.infoBar.Dismiss() + def OnReloadTree(self, event): """Reload whole tree""" self.LoadItems() @@ -135,6 +164,12 @@ def OnDownloadLocation(self, event): db_node, loc_node, mapset_node = self.tree.GetCurrentDbLocationMapsetNode() 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): """Convert multiple GDAL layers to GRASS raster map layers""" from modules.import_export import GdalImportDialog diff --git a/gui/wxpython/datacatalog/infomanager.py b/gui/wxpython/datacatalog/infomanager.py index da277e58630..56cad9d3601 100644 --- a/gui/wxpython/datacatalog/infomanager.py +++ b/gui/wxpython/datacatalog/infomanager.py @@ -20,6 +20,7 @@ import wx from grass.script import gisenv +from grass.grassdb.checks import get_mapset_owner class DataCatalogInfoManager: @@ -31,8 +32,10 @@ def __init__(self, infobar, giface): def ShowDataStructureInfo(self, onCreateLocationHandler): """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 = _( "GRASS GIS helps you organize your data using Locations (projects) " "which contain Mapsets (subprojects). All data in one Location is " @@ -41,13 +44,15 @@ def ShowDataStructureInfo(self, onCreateLocationHandler): "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 " "the toolbar above." - ).format(loc=gisenv()['LOCATION_NAME']) + ).format(loc=gisenv()["LOCATION_NAME"]) self.infoBar.ShowMessage(message, wx.ICON_INFORMATION, buttons) def ShowImportDataInfo(self, OnImportOgrLayersHandler, OnImportGdalLayersHandler): """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 = _( "You have successfully created a new Location {loc}. " "Currently you are in its PERMANENT Mapset which is used for " @@ -55,8 +60,50 @@ def ShowImportDataInfo(self, OnImportOgrLayersHandler, OnImportGdalLayersHandler "Mapsets. You can create new Mapsets for different tasks by right " "clicking on the Location name.\n\n" "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) + 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): self._giface.Help(entry="grass_database") diff --git a/gui/wxpython/datacatalog/tree.py b/gui/wxpython/datacatalog/tree.py index c2b7874713b..2192364d3e1 100644 --- a/gui/wxpython/datacatalog/tree.py +++ b/gui/wxpython/datacatalog/tree.py @@ -74,8 +74,7 @@ from grass.script import gisenv from grass.grassdb.data import map_exists 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 @@ -957,7 +956,7 @@ def CreateLocation(self, grassdb_node): """ Creates new location interactively and adds it to the tree and switch 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 = ( create_location_interactively(self, grassdb_node.data['name']) @@ -968,14 +967,14 @@ def CreateLocation(self, grassdb_node): element='location', 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 self.SwitchMapset(grassdatabase, location, mapset, 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): """Create new location""" self.CreateLocation(self.selected_grassdb[0]) @@ -1560,6 +1559,11 @@ def _updateAfterGrassdbChanged(self, action, element, grassdb, location, mapset= location=location) if node: 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'): # when watchdog is used, it watches current mapset, # so we don't process any signals here, diff --git a/gui/wxpython/gis_set.py b/gui/wxpython/gis_set.py deleted file mode 100644 index 2ca305c6e58..00000000000 --- a/gui/wxpython/gis_set.py +++ /dev/null @@ -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 (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 == "" 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 ( comes from init script) - if self.GetRCValue("LOCATION_NAME") != "": - 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() diff --git a/gui/wxpython/gis_set_error.py b/gui/wxpython/gis_set_error.py deleted file mode 100644 index 7bd93fe5806..00000000000 --- a/gui/wxpython/gis_set_error.py +++ /dev/null @@ -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 -""" - -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() diff --git a/gui/wxpython/lmgr/frame.py b/gui/wxpython/lmgr/frame.py index ff140f48029..3f6a6c73712 100644 --- a/gui/wxpython/lmgr/frame.py +++ b/gui/wxpython/lmgr/frame.py @@ -37,9 +37,6 @@ from grass.script import core as grass from grass.script.utils import decode -from startup.guiutils import ( - can_switch_mapset_interactive -) from core.gcmd import RunCommand, GError, GMessage from core.settings import UserSettings, GetDisplayVectSettings @@ -63,12 +60,13 @@ from datacatalog.catalog import DataCatalog from gui_core.forms import GUI from gui_core.wrap import Menu, TextEntryDialog -from grass.grassdb.checks import is_current_mapset_in_demolocation from startup.guiutils import ( + can_switch_mapset_interactive, switch_mapset_interactively, create_mapset_interactively, create_location_interactively ) +from grass.grassdb.checks import is_first_time_user class GMFrame(wx.Frame): @@ -446,7 +444,7 @@ def show_demo(): lchecked=True, 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. wx.CallLater(1000, show_demo) diff --git a/gui/wxpython/startup/guiutils.py b/gui/wxpython/startup/guiutils.py index dd18330dd21..f0433e33894 100644 --- a/gui/wxpython/startup/guiutils.py +++ b/gui/wxpython/startup/guiutils.py @@ -16,7 +16,6 @@ import os -import sys import wx from grass.grassdb.checks import ( @@ -30,8 +29,10 @@ get_reasons_mapsets_not_removable, get_reasons_location_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.manage import ( @@ -43,21 +44,14 @@ ) from grass.script.core import create_environment 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 location_wizard.dialogs import RegionDef 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): def __init__(self, parent=None, default=None, message=None, caption=None, database=None, location=None): @@ -112,57 +106,6 @@ def _isLocationNameValid(self, 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): """ Create new mapset @@ -717,6 +660,9 @@ def import_file(guiparent, filePath, env): def switch_mapset_interactively(guiparent, giface, dbase, location, mapset, show_confirmation=False): """Switch current mapset. Emits giface.currentMapsetChanged signal.""" + # Decide if a user is in a fallback session + fallback_session = is_fallback_session() + if dbase: if RunCommand('g.mapset', parent=guiparent, location=location, @@ -751,4 +697,17 @@ def switch_mapset_interactively(guiparent, giface, dbase, location, mapset, if show_confirmation: GMessage(parent=guiparent, 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" + ) diff --git a/lib/init/grass.py b/lib/init/grass.py index e50e7cb0cf4..c36788ea896 100755 --- a/lib/init/grass.py +++ b/lib/init/grass.py @@ -586,6 +586,21 @@ def read_gisrc(filename): 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): kv = {} f = open(path, "r") @@ -605,7 +620,7 @@ def write_gisrc(kv, filename, append=False): 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): kv = read_gisrc(gisrc) else: @@ -616,6 +631,27 @@ def set_mapset_to_gisrc(gisrc, grassdb, location, mapset): 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): grass_gui = None # 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) - set_mapset_to_gisrc(gisrc, gisdbase, location_name, mapset) + add_mapset_to_gisrc(gisrc, gisdbase, location_name, mapset) else: 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 # translate to it, so always double check class MapsetSettings(object): @@ -1276,6 +1241,18 @@ def is_valid(self): 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? # anything could have happened in between loading from gisrcrc and now # (we do e.g. GUI or creating loctation) @@ -1284,12 +1261,8 @@ def load_gisrc(gisrc, gisrcrc): :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( _( "Error reading data path information from g.gisenv.\n" @@ -1307,22 +1280,6 @@ def load_gisrc(gisrc, gisrcrc): 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 def load_env(grass_env_file): 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 unresolved. """ + from grass.grassdb.checks import is_mapset_valid + if not os.path.exists(mapset_path): fatal(_("Path '%s' doesn't exist") % mapset_path) if not os.access(mapset_path, os.W_OK): @@ -2555,39 +2514,71 @@ def main(): save_gui(gisrc, grass_gui) # Parsing argument to get LOCATION + # Mapset is not specified in command line arguments 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}") 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( _( - "<{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: # Mapset was specified in command line parameters. if params.tmp_location: @@ -2627,12 +2618,8 @@ def main(): force_gislock_removal=params.force_gislock_removal, ) 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 # in case mapset was changed @@ -2714,8 +2701,11 @@ def main(): # here we are at the end of grass session 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 # done message at last: no atexit.register() # or register done_message() diff --git a/python/grass/app/data.py b/python/grass/app/data.py index c0ee93a9b29..5f504497ce4 100644 --- a/python/grass/app/data.py +++ b/python/grass/app/data.py @@ -17,6 +17,7 @@ import getpass import sys from shutil import copytree, ignore_patterns +import grass.grassdb.config as cfg from grass.grassdb.checks import is_location_valid @@ -141,18 +142,25 @@ def create_startup_location_in_grassdb(grassdatabase, startup_location_name): 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 diff --git a/python/grass/grassdb/Makefile b/python/grass/grassdb/Makefile index 3b6fa9548c5..ce3d6fb3d01 100644 --- a/python/grass/grassdb/Makefile +++ b/python/grass/grassdb/Makefile @@ -5,7 +5,7 @@ include $(MODULE_TOPDIR)/include/Make/Python.make DSTDIR = $(ETC)/python/grass/grassdb -MODULES = checks create data manage +MODULES = checks create data manage config PYFILES := $(patsubst %,$(DSTDIR)/%.py,$(MODULES) __init__) PYCFILES := $(patsubst %,$(DSTDIR)/%.pyc,$(MODULES) __init__) diff --git a/python/grass/grassdb/checks.py b/python/grass/grassdb/checks.py index 0b4fe01512a..9b233c91bdc 100644 --- a/python/grass/grassdb/checks.py +++ b/python/grass/grassdb/checks.py @@ -9,7 +9,6 @@ .. sectionauthor:: Vaclav Petras """ - import os import sys import datetime @@ -18,6 +17,8 @@ import grass.script as gs import glob +import grass.grassdb.config as cfg + def mapset_exists(database, location, mapset): """Returns True whether mapset path exists.""" @@ -115,8 +116,33 @@ def get_mapset_owner(mapset_path): 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): @@ -169,6 +195,27 @@ def can_start_in_mapset(mapset_path, ignore_lock=False): 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): """Return True if directory *path* contains a valid location""" if not os.path.isdir(path): diff --git a/python/grass/grassdb/config.py b/python/grass/grassdb/config.py new file mode 100644 index 00000000000..8e1a83e8996 --- /dev/null +++ b/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 +""" + +# global variables +default_location = "world_latlong_wgs84" +temporary_location = "tmploc" +unknown_location = "" +unknown_mapset = "" +permanent_mapset = "PERMANENT" diff --git a/python/grass/grassdb/manage.py b/python/grass/grassdb/manage.py index 54dfc755711..ef92c109e3e 100644 --- a/python/grass/grassdb/manage.py +++ b/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): """Rename location from *old_name* to *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