diff --git a/gui/wxpython/datacatalog/catalog.py b/gui/wxpython/datacatalog/catalog.py index c8b02422e1b..f6e057b583c 100644 --- a/gui/wxpython/datacatalog/catalog.py +++ b/gui/wxpython/datacatalog/catalog.py @@ -73,6 +73,10 @@ def __init__( # tree with layers self.tree = DataCatalogTree(self, giface=giface) self.tree.showNotification.connect(self.showNotification) + self.tree.showDownloadingNewLocationInfoMessage.connect( + self.showDownloadingNewLocationInfoMessage + ) + self.tree.dismissShowInfoBarMessage.connect(self.dismissInfobar) # infobar for data catalog delay = 2000 @@ -139,6 +143,18 @@ def showImportDataInfo(self): self.OnImportOgrLayers, self.OnImportGdalLayers ) + def showDownloadingNewLocationInfoMessage(self, message, buttons): + """Show progress of downloading new location info message + + :param str message: text message + :param list buttons: list of info bar widget buttons + [(button_label, button_click_event_handler),...] + """ + self.infoManager.ShowDownloadingNewLocationInfo( + message=message, + buttons=buttons, + ) + def LoadItems(self): """Reload tree - full or lazy - based on user settings""" self._startLoadingTime = clock() diff --git a/gui/wxpython/datacatalog/infomanager.py b/gui/wxpython/datacatalog/infomanager.py index 086211f5f8f..67c011ad238 100644 --- a/gui/wxpython/datacatalog/infomanager.py +++ b/gui/wxpython/datacatalog/infomanager.py @@ -100,6 +100,23 @@ def ShowLockedMapsetInfo(self, OnSwitchMapsetHandler): ).format(mapsetpath=last_used_mapset_path) self.infoBar.ShowMessage(message, wx.ICON_INFORMATION, buttons) + def ShowDownloadingNewLocationInfo(self, message, buttons): + """Show progress of downloading new location info message + + :param str message: message text + :param list buttons: list of info bar widget buttons + [(buton_label, button_click_event_handler),...] + """ + self.infoBar.ShowMessage(message, wx.ICON_INFORMATION, remove_buttons=False) + if buttons: + for btn in buttons: + if btn["btnsAction"]["addBtn"]: + # Add btn widget here, to prevent blinking widget + self.infoBar.AddButton(btnid=btn["id"], label=btn["label"]) + self.infoBar.Bind(wx.EVT_BUTTON, btn["handler"], id=btn["id"]) + if btn["btnsAction"]["removeBtn"]: + self.infoBar.RemoveButtons() + 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"] diff --git a/gui/wxpython/datacatalog/tree.py b/gui/wxpython/datacatalog/tree.py index 76aee7e5eff..6cc53e41106 100644 --- a/gui/wxpython/datacatalog/tree.py +++ b/gui/wxpython/datacatalog/tree.py @@ -232,6 +232,10 @@ def __init__( self.showNotification = Signal("Tree.showNotification") self.showImportDataInfo = Signal("Tree.showImportDataInfo") self.loadingDone = Signal("Tree.loadingDone") + self.showDownloadingNewLocationInfoMessage = Signal( + "Tree.showDownloadingNewLocationInfoMessage" + ) + self.dismissShowInfoBarMessage = Signal("Tree.dismissShowInfoBarMessage") self.parent = parent self.contextMenu.connect(self.OnRightClick) self.itemActivated.connect(self.OnDoubleClick) @@ -1612,17 +1616,35 @@ def OnDeleteLocation(self, event): for change in changes: self._giface.grassdbChanged.emit(**change) + def AddDownloadedNewLocation(self): + """ + Add downloaded new location into tree + """ + location = self.loc_download.GetLocation() + if location: + self._reloadGrassDBNode(self.selected_grassdb[0]) + self.UpdateCurrentDbLocationMapsetNode() + self.RefreshItems() + self.showNotification.emit( + message=_( + "The new location <{}> has been downloaded successfully" + ).format(location) + ) + def DownloadLocation(self, grassdb_node): """ Download new location interactively. """ - grassdatabase, location, mapset = download_location_interactively( + self.loc_download = download_location_interactively( self, grassdb_node.data["name"] ) - if location: - self._reloadGrassDBNode(grassdb_node) - self.UpdateCurrentDbLocationMapsetNode() - self.RefreshItems() + self.loc_download.newLocationIsDownloaded.connect(self.AddDownloadedNewLocation) + self.loc_download.showInfoBarMessage.connect( + self.showDownloadingNewLocationInfoMessage + ) + self.loc_download.dismissShowInfoBarMessage.connect( + self.dismissShowInfoBarMessage + ) def OnDownloadLocation(self, event): """ diff --git a/gui/wxpython/gui_core/infobar.py b/gui/wxpython/gui_core/infobar.py index e9bd8b848a5..1353ea1a015 100644 --- a/gui/wxpython/gui_core/infobar.py +++ b/gui/wxpython/gui_core/infobar.py @@ -99,11 +99,12 @@ def _unInitLayout(self): else: sizer.Detach(i) - def ShowMessage(self, message, icon, buttons=None): + def ShowMessage(self, message, icon, buttons=None, remove_buttons=True): """Show message with buttons (optional). Buttons are list of tuples (label, handler)""" self.Hide() - self.RemoveButtons() + if remove_buttons: + self.RemoveButtons() if buttons: self.SetButtons(buttons) super().ShowMessage(message, icon) @@ -122,6 +123,7 @@ def AddButton(self, btnid, label): self._button.Hide() button = wx.Button(self, btnid, label) + self.button_ids.append(btnid) button.SetBackgroundColour(self._background_color) button.SetForegroundColour(self._foreground_color) diff --git a/gui/wxpython/startup/guiutils.py b/gui/wxpython/startup/guiutils.py index de390ec5257..500481de642 100644 --- a/gui/wxpython/startup/guiutils.py +++ b/gui/wxpython/startup/guiutils.py @@ -317,23 +317,17 @@ def download_location_interactively(guiparent, grassdb): """ Download new location using Location Wizard. - Returns tuple (database, location, mapset) where mapset is "PERMANENT" - by default or in future it could be the mapset the user may want to - switch to. + :param guiparent object: gui parent object + :param str grassdb: GRASS GIS database path string + + :return object loc_download: Download location dialog instance object """ from startup.locdownload import LocationDownloadDialog - result = (None, None, None) loc_download = LocationDownloadDialog(parent=guiparent, database=grassdb) loc_download.Centre() - loc_download.ShowModal() - - if loc_download.GetLocation() is not None: - # Returns database and location created by user - # and a mapset user may want to switch to - result = (grassdb, loc_download.GetLocation(), "PERMANENT") - loc_download.Destroy() - return result + loc_download.Show() + return loc_download def delete_mapset_interactively(guiparent, grassdb, location, mapset): diff --git a/gui/wxpython/startup/locdownload.py b/gui/wxpython/startup/locdownload.py index 3fb45c05ab9..b842d15b2aa 100644 --- a/gui/wxpython/startup/locdownload.py +++ b/gui/wxpython/startup/locdownload.py @@ -25,6 +25,7 @@ import wx from wx.lib.newevent import NewEvent +from grass.pydispatch.signal import Signal from grass.script.utils import try_rmdir, legalize_vector_name from grass.utils.download import download_and_extract, name_from_url, DownloadError from grass.grassdb.checks import is_location_valid @@ -87,17 +88,18 @@ class RedirectText: def __init__(self, window): - self.out = window + self.window = window def write(self, string): try: - if self.out: + if self.window.message: string = self._wrap_string(string) height = self._get_height(string) - wx.CallAfter(self.out.SetLabel, string) + wx.CallAfter(self.window.message.SetLabel, string) + # Show Data Catalog info bar messsage + wx.CallAfter(self.window.ShowInfoBarMessage, string) self._resize(height) except (RuntimeError, AttributeError): - # window closed or destroyed pass def flush(self): @@ -122,7 +124,7 @@ def _get_height(self, string): :return int: widget height """ n_lines = string.count("\n") - attr = self.out.GetClassDefaultAttributes() + attr = self.window.message.GetClassDefaultAttributes() font_size = attr.font.GetPointSize() return int((n_lines + 2) * font_size // 0.75) # 1 px = 0.75 pt @@ -131,11 +133,11 @@ def _resize(self, height=-1): :param int height: widget height """ - wx.CallAfter(self.out.GetParent().SetMinSize, (-1, -1)) - wx.CallAfter(self.out.SetMinSize, (-1, height)) + wx.CallAfter(self.window.message.GetParent().SetMinSize, (-1, -1)) + wx.CallAfter(self.window.message.SetMinSize, (-1, height)) wx.CallAfter( - self.out.GetParent().parent.sizer.Fit, - self.out.GetParent().parent, + self.window.message.GetParent().parent.sizer.Fit, + self.window.message.GetParent().parent, ) @@ -225,7 +227,12 @@ def __init__(self, parent, database, locations=LOCATIONS): self.database = database self.locations = locations self._abort_btn_label = _("Abort") - self._abort_btn_tooltip = _("Abort download") + self._abort_btn_tooltip = _("Abort download location") + self._infobar_message_btn_id = wx.NewIdRef() + self._infobar_message_btns = { + "addBtn": None, + "removeBtn": None, + } self.label = StaticText( parent=self, label=_("Select sample project to download:") @@ -235,13 +242,14 @@ def __init__(self, parent, database, locations=LOCATIONS): self.choice = wx.Choice(parent=self, choices=choices) self.choice.Bind(wx.EVT_CHOICE, self.OnChangeChoice) - self.parent.download_button.Bind(wx.EVT_BUTTON, self.OnDownload) + if hasattr(self.parent, "download_button"): + self.parent.download_button.Bind(wx.EVT_BUTTON, self.OnDownload) # TODO: add button for a link to an associated website? # TODO: add thumbnail for each location? # TODO: messages copied from gis_set.py, need this as API? self.message = StaticText(parent=self, size=(-1, 50)) - sys.stdout = RedirectText(self.message) + sys.stdout = RedirectText(window=self) # 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 @@ -299,8 +307,40 @@ def _change_download_btn_label( self.parent.download_button.SetLabel(label) self.parent.download_button.SetToolTip(tooltip) + def _terminateDownloadCallback(self, event): + """Terminate download callback + + :param object event: event object + """ + self._infobar_message_btns["addBtn"] = True + if self._download_in_progress: + self.thread.Terminate() + # Clean up after urllib urlretrieve which is used internally + # in grass.utils. + from urllib import request # pylint: disable=import-outside-toplevel + + self._download_in_progress = False + request.urlcleanup() + sys.stdout.write("Download aborted") + self.thread = gThread() + self._change_download_btn_label() + self.parent.Show(True) + self._infobar_message_btns.update( + dict(zip(("addBtn", "removeBtn"), (False, True))), + ) + + def OnAbort(self, event): + """Info bar widget abort button event handler + + :param object event: event object + """ + self._terminateDownloadCallback(event) + def OnDownload(self, event): """Handle user-initiated action of download""" + self._infobar_message_btns.update( + dict(zip(("addBtn", "removeBtn"), (True, False))), + ) button_label = self.parent.download_button.GetLabel() if button_label in {_("Download"), _("Do&wnload")}: self._change_download_btn_label( @@ -315,6 +355,32 @@ def OnDownload(self, event): else: self.parent.OnCancel() + def ShowInfoBarMessage(self, message, buttons=None): + """Show progress of downloading new location Data Catalog info + message + + :param str message: text message + :param list buttons: list of info bar widget buttons + [(button_label, button_click_event_handler),...] + """ + if hasattr(self.parent, "showInfoBarMessage"): + self.parent.showInfoBarMessage.emit( + message=message, + buttons=( + [ + { + "id": self._infobar_message_btn_id, + "label": self._abort_btn_label, + "handler": self.OnAbort, + "btnsAction": self._infobar_message_btns, + } + ] + if buttons is None + else buttons + ), + ) + self._infobar_message_btns["addBtn"] = False + def DownloadItem(self, item): """Download the selected item""" Debug.msg(1, "DownloadItem: %s" % item) @@ -328,11 +394,15 @@ def DownloadItem(self, item): "Project name {name} already exists in {path}, download canceled" ).format(name=dirname, path=self.database) ) + self._infobar_message_btns.update( + dict(zip(("addBtn", "removeBtn"), (False, True))), + ) self._change_download_btn_label() return def download_complete_callback(event): self._download_in_progress = False + self._infobar_message_btns["addBtn"] = True errors = event.ret if errors: self._error(_("Download failed: %s") % errors) @@ -340,22 +410,17 @@ def download_complete_callback(event): self._last_downloaded_location_name = dirname self._warning( _( - "Download completed. The downloaded sample data is available " - "now in the data tree" + "Download completed. The downloaded sample data is listed " + "in the location/mapset tree." ) ) + self.parent.newLocationIsDownloaded.emit() + self._infobar_message_btns.update( + dict(zip(("addBtn", "removeBtn"), (False, True))), + ) self._change_download_btn_label() - - def terminate_download_callback(event): - # Clean up after urllib urlretrieve which is used internally - # in grass.utils. - from urllib import request # pylint: disable=import-outside-toplevel - - self._download_in_progress = False - request.urlcleanup() - sys.stdout.write("Download aborted") - self.thread = gThread() - self._change_download_btn_label() + if errors: + self.parent.Show(True) self._download_in_progress = True self._warning(_("Download in progress, wait until it is finished")) @@ -365,8 +430,9 @@ def terminate_download_callback(event): name=dirname, database=self.database, ondone=download_complete_callback, - onterminate=terminate_download_callback, + onterminate=self._terminateDownloadCallback, ) + wx.CallLater(1000, self.parent.Show, False) def OnChangeChoice(self, event): """React to user changing the selection""" @@ -376,6 +442,7 @@ def OnChangeChoice(self, event): def CheckItem(self, item): """Check what user selected and report potential issues""" # similar code as in DownloadItem + self._infobar_message_btns["addBtn"] = True url = item["url"] dirname = location_name_from_url(url) destination = os.path.join(self.database, dirname) @@ -385,8 +452,12 @@ def CheckItem(self, item): name=dirname ) ) - self.parent.download_button.SetLabel(label=_("Download")) + self._infobar_message_btns["addBtn"] = False + if hasattr(self.parent, "download_button"): + self.parent.download_button.SetLabel(label=_("Download")) return + if hasattr(self.parent, "dismissShowInfoBarMessage"): + self.parent.dismissShowInfoBarMessage.emit() self._clearMessage() def GetLocation(self): @@ -441,11 +512,20 @@ def __init__(self, parent, database, title=_("Dataset Download")): :param title: window title if the default is not appropriate """ wx.Dialog.__init__(self, parent=parent, title=title) - cancel_button = Button(self, id=wx.ID_CANCEL) + + self.newLocationIsDownloaded = Signal( + "LocationDownloadDialog.newLocationIsDownloaded" + ) + self.showInfoBarMessage = Signal("LocationDownloadDialog.showInfoBarMessage") + self.dismissShowInfoBarMessage = Signal( + "LocationDownloadDialog.dismissShowInfoBarMessage" + ) + + self.cancel_button = Button(self, id=wx.ID_CANCEL) self.download_button = Button(parent=self, id=wx.ID_ANY, label=_("Do&wnload")) self.download_button.SetToolTip(_("Download selected dataset")) self.panel = LocationDownloadPanel(parent=self, database=database) - cancel_button.Bind(wx.EVT_BUTTON, self.OnCancel) + self.cancel_button.Bind(wx.EVT_BUTTON, self.OnCancel) self.Bind(wx.EVT_CLOSE, self.OnCancel) self.sizer = wx.BoxSizer(wx.VERTICAL) @@ -453,7 +533,7 @@ def __init__(self, parent, database, title=_("Dataset Download")): button_sizer = wx.StdDialogButtonSizer() button_sizer.Add( - cancel_button, + self.cancel_button, proportion=0, flag=wx.EXPAND | wx.LEFT | wx.RIGHT, border=5, @@ -500,7 +580,8 @@ def OnCancel(self, event=None): self.panel._change_download_btn_label() if event: - self.EndModal(wx.ID_CANCEL) + self.dismissShowInfoBarMessage.emit() + self.Destroy() def main(): @@ -512,12 +593,21 @@ def main(): app = wx.App() if len(sys.argv) == 2 or sys.argv[2] == "dialog": - window = LocationDownloadDialog(parent=None, database=database) - window.ShowModal() - location = window.GetLocation() - if location: - print(location) - window.Destroy() + + def new_location_is_downloaded(): + # Reset stdout + sys.stdout = sys.__stdout__ + print(window.GetLocation()) + + def on_close(event): + parent_window.Destroy() + + parent_window = wx.Dialog(parent=None) + window = LocationDownloadDialog(parent=parent_window, database=database) + window.Bind(wx.EVT_CLOSE, on_close) + window.cancel_button.Bind(wx.EVT_BUTTON, on_close) + window.newLocationIsDownloaded.connect(new_location_is_downloaded) + window.Show() elif sys.argv[2] == "panel": window = wx.Dialog(parent=None) panel = LocationDownloadPanel(parent=window, database=database)