diff --git a/.flake8 b/.flake8 index 73ea8346559..f340bac1ac9 100644 --- a/.flake8 +++ b/.flake8 @@ -69,7 +69,7 @@ per-file-ignores = gui/wxpython/gcp/g.gui.gcp.py: F841 gui/wxpython/gcp/manager.py: E501, F841, E722 gui/wxpython/gcp/mapdisplay.py: F841 - gui/wxpython/gmodeler/*: F841, E722, W605, F405, F403, E402 + gui/wxpython/gmodeler/*: E501 gui/wxpython/gui_core/*: F841, E266, E722, W605 gui/wxpython/gui_core/dialogs.py: E501, E722, F841, W605 gui/wxpython/gui_core/forms.py: E501, E722, F841 diff --git a/gui/icons/grass/modeler-settings.png b/gui/icons/grass/modeler-settings.png new file mode 100644 index 00000000000..11a07609cbe Binary files /dev/null and b/gui/icons/grass/modeler-settings.png differ diff --git a/gui/wxpython/.pylintrc b/gui/wxpython/.pylintrc index 6268e8467a7..7bfe0db218d 100644 --- a/gui/wxpython/.pylintrc +++ b/gui/wxpython/.pylintrc @@ -319,7 +319,7 @@ ignore-on-opaque-inference=yes # List of class names for which member attributes should not be checked (useful # for classes with dynamically set attributes). This supports the use of # qualified names. -ignored-classes=optparse.Values,thread._local,_thread._local +ignored-classes=optparse.Values,thread._local,_thread._local,main_window.page.MainPageBase # List of module names for which member attributes should not be checked # (useful for modules/projects where namespaces are manipulated during runtime diff --git a/gui/wxpython/gmodeler/canvas.py b/gui/wxpython/gmodeler/canvas.py new file mode 100644 index 00000000000..00a71fadc7c --- /dev/null +++ b/gui/wxpython/gmodeler/canvas.py @@ -0,0 +1,512 @@ +""" +@package gmodeler.canvas + +@brief wxGUI Graphical Modeler for creating, editing, and managing models + +Classes: + - canvas::ModelCanvas + - canvas::ModelEvtHandler + +(C) 2010-2023 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 +@author Python exports Ondrej Pesek +""" + +import wx +from wx.lib import ogl + +from gui_core.dialogs import TextEntryDialog as CustomTextEntryDialog +from gui_core.wrap import TextEntryDialog as wxTextEntryDialog, NewId, Menu +from gui_core.forms import GUI +from core.gcmd import GException, GError + +from gmodeler.model import ( + ModelRelation, + ModelAction, + ModelData, + ModelLoop, + ModelCondition, + ModelComment, +) +from gmodeler.dialogs import ( + ModelRelationDialog, + ModelDataDialog, + ModelLoopDialog, + ModelConditionDialog, +) +from gmodeler.giface import GraphicalModelerGrassInterface + + +class ModelCanvas(ogl.ShapeCanvas): + """Canvas where model is drawn""" + + def __init__(self, parent): + self.parent = parent + ogl.OGLInitialize() + ogl.ShapeCanvas.__init__(self, parent) + + self.diagram = ogl.Diagram() + self.SetDiagram(self.diagram) + self.diagram.SetCanvas(self) + + self.SetScrollbars(20, 20, 2000 // 20, 2000 // 20) + + self.Bind(wx.EVT_KEY_UP, self.OnKeyUp) + self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) + + def OnKeyUp(self, event): + """Key pressed""" + kc = event.GetKeyCode() + if kc == wx.WXK_DELETE: + self.RemoveSelected() + + def OnLeftDown(self, evt): + self.SetFocus() + evt.Skip() + + def RemoveSelected(self): + """Remove selected shapes""" + self.parent.ModelChanged() + + diagram = self.GetDiagram() + shapes = [shape for shape in diagram.GetShapeList() if shape.Selected()] + self.RemoveShapes(shapes) + + def RemoveShapes(self, shapes): + """Removes shapes""" + self.parent.ModelChanged() + diagram = self.GetDiagram() + for shape in shapes: + remList, upList = self.parent.GetModel().RemoveItem(shape) + shape.Select(False) + diagram.RemoveShape(shape) + shape.__del__() + for item in remList: + diagram.RemoveShape(item) + item.__del__() + + for item in upList: + item.Update() + + self.Refresh() + + def GetNewShapePos(self, yoffset=50): + """Determine optimal position for newly added object + + :return: x,y + """ + ymax = 20 + for item in self.GetDiagram().GetShapeList(): + y = item.GetY() + item.GetBoundingBoxMin()[1] + if y > ymax: + ymax = y + + return (self.GetSize()[0] // 2, ymax + yoffset) + + def GetShapesSelected(self): + """Get list of selected shapes""" + selected = list() + diagram = self.GetDiagram() + for shape in diagram.GetShapeList(): + if shape.Selected(): + selected.append(shape) + + return selected + + +class ModelEvtHandler(ogl.ShapeEvtHandler): + """Model event handler class""" + + def __init__(self, log, frame): + ogl.ShapeEvtHandler.__init__(self) + self.log = log + self.frame = frame + self.x = self.y = None + + def OnLeftClick(self, x, y, keys=0, attachment=0): + """Left mouse button pressed -> select item & update statusbar""" + shape = self.GetShape() + + # probably does nothing, removed from wxPython 2.9 + # canvas = shape.GetCanvas() + # dc = wx.ClientDC(canvas) + # canvas.PrepareDC(dc) + + if hasattr(self.frame, "defineRelation"): + drel = self.frame.defineRelation + if drel["from"] is None: + drel["from"] = shape + elif drel["to"] is None: + drel["to"] = shape + rel = ModelRelation( + parent=self.frame, fromShape=drel["from"], toShape=drel["to"] + ) + dlg = ModelRelationDialog(parent=self.frame, shape=rel) + if dlg.IsValid(): + ret = dlg.ShowModal() + if ret == wx.ID_OK: + option = dlg.GetOption() + rel.SetName(option) + drel["from"].AddRelation(rel) + drel["to"].AddRelation(rel) + drel["from"].Update() + params = { + "params": [ + {"name": option, "value": drel["from"].GetValue()} + ] + } + drel["to"].MergeParams(params) + self.frame.AddLine(rel) + + dlg.Destroy() + del self.frame.defineRelation + + # select object + self._onSelectShape(shape, append=True if keys == 1 else False) + + if hasattr(shape, "GetLog"): + self.log.SetStatusText(shape.GetLog(), 0) + else: + self.log.SetStatusText("", 0) + + def OnLeftDoubleClick(self, x, y, keys=0, attachment=0): + """Left mouse button pressed (double-click) -> show properties""" + self.OnProperties() + + def OnProperties(self, event=None): + """Show properties dialog""" + self.frame.ModelChanged() + shape = self.GetShape() + if isinstance(shape, ModelAction): + gmodule = GUI( + parent=self.frame, + show=True, + giface=GraphicalModelerGrassInterface(self.frame.GetModel()), + ) + gmodule.ParseCommand( + shape.GetLog(string=False), + completed=(self.frame.GetOptData, shape, shape.GetParams()), + ) + + elif isinstance(shape, ModelData): + if shape.GetPrompt() in ( + "raster", + "vector", + "raster_3d", + "stds", + "strds", + "stvds", + "str3ds", + ): + dlg = ModelDataDialog(parent=self.frame, shape=shape) + shape.SetPropDialog(dlg) + dlg.CentreOnParent() + dlg.Show() + + elif isinstance(shape, ModelLoop): + dlg = ModelLoopDialog(parent=self.frame, shape=shape) + dlg.CentreOnParent() + if dlg.ShowModal() == wx.ID_OK: + shape.SetLabel(dlg.GetCondition()) + model = self.frame.GetModel() + ids = dlg.GetItems() + alist = list() + for aId in ids["unchecked"]: + action = model.GetItem(aId, objType=ModelAction) + if action: + action.UnSetBlock(shape) + for aId in ids["checked"]: + action = model.GetItem(aId, objType=ModelAction) + if action: + action.SetBlock(shape) + alist.append(aId) + shape.SetItems(alist) + self.frame.DefineLoop(shape) + self.frame.SetStatusText(shape.GetLog(), 0) + self.frame.GetCanvas().Refresh() + + dlg.Destroy() + + elif isinstance(shape, ModelCondition): + dlg = ModelConditionDialog(parent=self.frame, shape=shape) + dlg.CentreOnParent() + if dlg.ShowModal() == wx.ID_OK: + shape.SetLabel(dlg.GetCondition()) + model = self.frame.GetModel() + ids = dlg.GetItems() + for b in ids.keys(): + alist = list() + for aId in ids[b]["unchecked"]: + action = model.GetItem(aId, objType=ModelAction) + action.UnSetBlock(shape) + for aId in ids[b]["checked"]: + action = model.GetItem(aId, objType=ModelAction) + action.SetBlock(shape) + if action: + alist.append(aId) + shape.SetItems(alist, branch=b) + self.frame.DefineCondition(shape) + self.frame.GetCanvas().Refresh() + + dlg.Destroy() + + def OnBeginDragLeft(self, x, y, keys=0, attachment=0): + """Drag shape (beginning)""" + self.frame.ModelChanged() + if self._previousHandler: + self._previousHandler.OnBeginDragLeft(x, y, keys, attachment) + + def OnEndDragLeft(self, x, y, keys=0, attachment=0): + """Drag shape (end)""" + if self._previousHandler: + self._previousHandler.OnEndDragLeft(x, y, keys, attachment) + + shape = self.GetShape() + if isinstance(shape, ModelLoop): + self.frame.DefineLoop(shape) + elif isinstance(shape, ModelCondition): + self.frame.DefineCondition(shape) + + for mo in shape.GetBlock(): + if isinstance(mo, ModelLoop): + self.frame.DefineLoop(mo) + elif isinstance(mo, ModelCondition): + self.frame.DefineCondition(mo) + + shape = self.GetShape() + canvas = shape.GetCanvas() + canvas.Refresh() + + def OnEndSize(self, x, y): + """Resize shape""" + self.frame.ModelChanged() + if self._previousHandler: + self._previousHandler.OnEndSize(x, y) + + def OnRightClick(self, x, y, keys=0, attachment=0): + """Right click -> pop-up menu""" + if not hasattr(self, "popupID"): + self.popupID = dict() + for key in ( + "remove", + "enable", + "addPoint", + "delPoint", + "intermediate", + "display", + "props", + "id", + "label", + "comment", + ): + self.popupID[key] = NewId() + + # record coordinates + self.x = x + self.y = y + + # select object + shape = self.GetShape() + self._onSelectShape(shape) + + popupMenu = Menu() + popupMenu.Append(self.popupID["remove"], _("Remove")) + self.frame.Bind(wx.EVT_MENU, self.OnRemove, id=self.popupID["remove"]) + if isinstance(shape, ModelAction) or isinstance(shape, ModelLoop): + if shape.IsEnabled(): + popupMenu.Append(self.popupID["enable"], _("Disable")) + self.frame.Bind(wx.EVT_MENU, self.OnDisable, id=self.popupID["enable"]) + else: + popupMenu.Append(self.popupID["enable"], _("Enable")) + self.frame.Bind(wx.EVT_MENU, self.OnEnable, id=self.popupID["enable"]) + if isinstance(shape, ModelAction) or isinstance(shape, ModelComment): + popupMenu.AppendSeparator() + if isinstance(shape, ModelAction): + popupMenu.Append(self.popupID["label"], _("Set label")) + self.frame.Bind(wx.EVT_MENU, self.OnSetLabel, id=self.popupID["label"]) + if isinstance(shape, ModelAction) or isinstance(shape, ModelComment): + popupMenu.Append(self.popupID["comment"], _("Set comment")) + self.frame.Bind(wx.EVT_MENU, self.OnSetComment, id=self.popupID["comment"]) + + if isinstance(shape, ModelRelation): + popupMenu.AppendSeparator() + popupMenu.Append(self.popupID["addPoint"], _("Add control point")) + self.frame.Bind(wx.EVT_MENU, self.OnAddPoint, id=self.popupID["addPoint"]) + popupMenu.Append(self.popupID["delPoint"], _("Remove control point")) + self.frame.Bind( + wx.EVT_MENU, self.OnRemovePoint, id=self.popupID["delPoint"] + ) + if len(shape.GetLineControlPoints()) == 2: + popupMenu.Enable(self.popupID["delPoint"], False) + + if isinstance(shape, ModelData): + popupMenu.AppendSeparator() + if ( + "@" not in shape.GetValue() + and len(self.GetShape().GetRelations("from")) > 0 + ): + popupMenu.Append( + self.popupID["intermediate"], _("Intermediate"), kind=wx.ITEM_CHECK + ) + if self.GetShape().IsIntermediate(): + popupMenu.Check(self.popupID["intermediate"], True) + + self.frame.Bind( + wx.EVT_MENU, self.OnIntermediate, id=self.popupID["intermediate"] + ) + + if self.frame._giface.GetMapDisplay(): + popupMenu.Append( + self.popupID["display"], _("Display"), kind=wx.ITEM_CHECK + ) + if self.GetShape().HasDisplay(): + popupMenu.Check(self.popupID["display"], True) + + self.frame.Bind( + wx.EVT_MENU, self.OnHasDisplay, id=self.popupID["display"] + ) + + if self.GetShape().IsIntermediate(): + popupMenu.Enable(self.popupID["display"], False) + + if ( + isinstance(shape, ModelData) + or isinstance(shape, ModelAction) + or isinstance(shape, ModelLoop) + ): + popupMenu.AppendSeparator() + popupMenu.Append(self.popupID["props"], _("Properties")) + self.frame.Bind(wx.EVT_MENU, self.OnProperties, id=self.popupID["props"]) + + self.frame.PopupMenu(popupMenu) + popupMenu.Destroy() + + def OnDisable(self, event): + """Disable action""" + self._onEnable(False) + + def OnEnable(self, event): + """Disable action""" + self._onEnable(True) + + def _onEnable(self, enable): + shape = self.GetShape() + shape.Enable(enable) + self.frame.ModelChanged() + self.frame.canvas.Refresh() + + def OnSetLabel(self, event): + shape = self.GetShape() + dlg = wxTextEntryDialog( + parent=self.frame, + message=_("Label:"), + caption=_("Set label"), + value=shape.GetLabel(), + ) + if dlg.ShowModal() == wx.ID_OK: + label = dlg.GetValue() + shape.SetLabel(label) + self.frame.ModelChanged() + self.frame.itemPanel.Update() + self.frame.canvas.Refresh() + dlg.Destroy() + + def OnSetComment(self, event): + shape = self.GetShape() + dlg = CustomTextEntryDialog( + parent=self.frame, + message=_("Comment:"), + caption=_("Set comment"), + defaultValue=shape.GetComment(), + textStyle=wx.TE_MULTILINE, + textSize=(300, 75), + ) + if dlg.ShowModal() == wx.ID_OK: + comment = dlg.GetValue() + shape.SetComment(comment) + self.frame.ModelChanged() + self.frame.canvas.Refresh() + dlg.Destroy() + + def _onSelectShape(self, shape, append=False): + canvas = shape.GetCanvas() + dc = wx.ClientDC(canvas) + + if shape.Selected(): + shape.Select(False, dc) + else: + shapeList = canvas.GetDiagram().GetShapeList() + toUnselect = list() + + if not append: + for s in shapeList: + if s.Selected(): + toUnselect.append(s) + + shape.Select(True, dc) + + for s in toUnselect: + s.Select(False, dc) + + canvas.Refresh(False) + + def OnAddPoint(self, event): + """Add control point""" + shape = self.GetShape() + shape.InsertLineControlPoint(point=wx.RealPoint(self.x, self.y)) + shape.ResetShapes() + shape.Select(True) + self.frame.ModelChanged() + self.frame.canvas.Refresh() + + def OnRemovePoint(self, event): + """Remove control point""" + shape = self.GetShape() + shape.DeleteLineControlPoint() + shape.Select(False) + shape.Select(True) + self.frame.ModelChanged() + self.frame.canvas.Refresh() + + def OnIntermediate(self, event): + """Mark data as intermediate""" + self.frame.ModelChanged() + shape = self.GetShape() + shape.SetIntermediate(event.IsChecked()) + self.frame.canvas.Refresh() + + def OnHasDisplay(self, event): + """Mark data to be displayed""" + self.frame.ModelChanged() + shape = self.GetShape() + shape.SetHasDisplay(event.IsChecked()) + self.frame.canvas.Refresh() + + try: + if event.IsChecked(): + # add map layer to display + self.frame._giface.GetLayerList().AddLayer( + ltype=shape.GetPrompt(), + name=shape.GetValue(), + checked=True, + cmd=shape.GetDisplayCmd(), + ) + else: + # remove map layer(s) from display + layers = self.frame._giface.GetLayerList().GetLayersByName( + shape.GetValue() + ) + for layer in layers: + self.frame._giface.GetLayerList().DeleteLayer(layer) + + except GException as e: + GError(parent=self, message="{}".format(e)) + + def OnRemove(self, event): + """Remove shape""" + self.frame.GetCanvas().RemoveShapes([self.GetShape()]) + self.frame.itemPanel.Update() diff --git a/gui/wxpython/gmodeler/dialogs.py b/gui/wxpython/gmodeler/dialogs.py index 8879bb8224e..b09720cbea5 100644 --- a/gui/wxpython/gmodeler/dialogs.py +++ b/gui/wxpython/gmodeler/dialogs.py @@ -35,7 +35,6 @@ from gui_core.dialogs import SimpleDialog, MapLayersDialogForModeler from gui_core.prompt import GPromptSTC from gui_core.gselect import Select, ElementSelect -from gmodeler.model import * from lmgr.menudata import LayerManagerMenuData from gui_core.wrap import ( Button, @@ -47,6 +46,7 @@ NewId, CheckListCtrlMixin, ) +from gmodeler.model import ModelData, ModelAction, ModelCondition class ModelDataDialog(SimpleDialog): @@ -885,7 +885,6 @@ def OnEndEdit(self, event): """Finish editing of item""" itemIndex = event.GetIndex() columnIndex = event.GetColumn() - nameOld = self.GetItem(itemIndex, 0).GetText() if columnIndex == 0: # TODO event.Veto() diff --git a/gui/wxpython/gmodeler/frame.py b/gui/wxpython/gmodeler/frame.py index 4539aa6e3bf..5ad7b6c34e0 100644 --- a/gui/wxpython/gmodeler/frame.py +++ b/gui/wxpython/gmodeler/frame.py @@ -4,14 +4,9 @@ @brief wxGUI Graphical Modeler for creating, editing, and managing models Classes: - - frame::ModelFrame - - frame::ModelCanvas - - frame::ModelEvtHandler - - frame::VariablePanel - - frame::ItemPanel - - frame::PythonPanel + - frame::ModelerFrame -(C) 2010-2018 by the GRASS Development Team +(C) 2010-2023 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. @@ -21,2256 +16,44 @@ """ import os -import sys -import time -import stat -import tempfile -import random -import math import wx -from wx.lib import ogl -from core import globalvar - -if globalvar.wxPythonPhoenix: - try: - import agw.flatnotebook as FN - except ImportError: # if it's not there locally, try the wxPython lib. - import wx.lib.agw.flatnotebook as FN -else: - import wx.lib.flatnotebook as FN -from wx.lib.newevent import NewEvent -from gui_core.widgets import GNotebook -from core.gconsole import GConsole, EVT_CMD_RUN, EVT_CMD_DONE, EVT_CMD_PREPARE -from gui_core.goutput import GConsoleWindow -from core.debug import Debug -from core.gcmd import GMessage, GException, GWarning, GError -from gui_core.dialogs import GetImageHandlers -from gui_core.dialogs import TextEntryDialog as CustomTextEntryDialog -from gui_core.ghelp import ShowAboutDialog -from core.settings import UserSettings +from core import globalvar from gui_core.menu import Menu as Menubar -from gmodeler.menudata import ModelerMenuData -from gui_core.forms import GUI -from gmodeler.preferences import PreferencesDialog, PropertiesDialog -from gmodeler.toolbars import ModelerToolbar -from core.giface import Notification -from gui_core.pystc import PyStc, SetDarkMode -from gmodeler.giface import GraphicalModelerGrassInterface -from gmodeler.model import * -from gmodeler.dialogs import * -from gui_core.wrap import ( - Button, - EmptyBitmap, - ImageFromBitmap, - Menu, - NewId, - StaticBox, - StaticText, - StockCursor, - TextCtrl, - IsDark, -) -from gui_core.wrap import TextEntryDialog as wxTextEntryDialog -wxModelDone, EVT_MODEL_DONE = NewEvent() - -from grass.script.utils import try_remove -from grass.script import core as grass +from gmodeler.menudata import ModelerMenuData +from gmodeler.panels import ModelerPanel -class ModelFrame(wx.Frame): +class ModelerFrame(wx.Frame): def __init__( self, parent, giface, id=wx.ID_ANY, title=_("Graphical Modeler"), **kwargs ): """Graphical modeler main window - :param parent: parent window + :param giface: GRASS interface :param id: window id :param title: window title :param kwargs: wx.Frames' arguments """ - self.parent = parent - self._giface = giface - self.searchDialog = None # module search dialog - self.baseTitle = title - self.modelFile = None # loaded model - self.start_time = None - self.modelChanged = False - self.randomness = 40 # random layout - - self.cursors = { - "default": StockCursor(wx.CURSOR_ARROW), - "cross": StockCursor(wx.CURSOR_CROSS), - } - wx.Frame.__init__(self, parent=parent, id=id, title=title, **kwargs) - self.SetName("Modeler") + self.SetIcon( wx.Icon(os.path.join(globalvar.ICONDIR, "grass.ico"), wx.BITMAP_TYPE_ICO) ) - self.menubar = Menubar( - parent=self, model=ModelerMenuData().GetModel(separators=True) - ) - self.SetMenuBar(self.menubar) - - self.toolbar = ModelerToolbar(parent=self) - # workaround for http://trac.wxwidgets.org/ticket/13888 - if sys.platform != "darwin": - self.SetToolBar(self.toolbar) - self.statusbar = self.CreateStatusBar(number=1) + self.panel = ModelerPanel(parent=self, giface=giface, statusbar=self.statusbar) - self.notebook = GNotebook(parent=self, style=globalvar.FNPageDStyle) - - self.canvas = ModelCanvas(self) - self.canvas.SetBackgroundColour( - wx.SystemSettings().GetColour(wx.SYS_COLOUR_WINDOW) - ) - self.canvas.SetCursor(self.cursors["default"]) - - self.model = Model(self.canvas) - - self.variablePanel = VariablePanel(parent=self) - - self.itemPanel = ItemPanel(parent=self) - - self.pythonPanel = PythonPanel(parent=self) - - self._gconsole = GConsole(guiparent=self, giface=giface) - self.goutput = GConsoleWindow( - parent=self, giface=giface, gconsole=self._gconsole - ) - self.goutput.showNotification.connect( - lambda message: self.SetStatusText(message) - ) - - # here events are binded twice - self._gconsole.Bind( - EVT_CMD_RUN, - lambda event: self._switchPageHandler( - event=event, notification=Notification.MAKE_VISIBLE - ), - ) - self._gconsole.Bind( - EVT_CMD_DONE, - lambda event: self._switchPageHandler( - event=event, notification=Notification.RAISE_WINDOW - ), - ) - self.Bind(EVT_CMD_RUN, self.OnCmdRun) - # rewrite default method to avoid hiding progress bar - self._gconsole.Bind(EVT_CMD_DONE, self.OnCmdDone) - self.Bind(EVT_CMD_PREPARE, self.OnCmdPrepare) - self.Bind(EVT_MODEL_DONE, self.OnModelDone) - - self.notebook.AddPage(page=self.canvas, text=_("Model"), name="model") - self.notebook.AddPage(page=self.itemPanel, text=_("Items"), name="items") - self.notebook.AddPage( - page=self.variablePanel, text=_("Variables"), name="variables" - ) - self.notebook.AddPage( - page=self.pythonPanel, text=_("Python editor"), name="python" - ) - self.notebook.AddPage( - page=self.goutput, text=_("Command output"), name="output" + self.menubar = Menubar( + parent=self, + model=ModelerMenuData().GetModel(separators=True), + class_handler=self.panel, ) - wx.CallAfter(self.notebook.SetSelectionByName, "model") - wx.CallAfter(self.ModelChanged, False) - - self.Bind(wx.EVT_CLOSE, self.OnCloseWindow) - self.Bind(wx.EVT_SIZE, self.OnSize) - self.notebook.Bind(FN.EVT_FLATNOTEBOOK_PAGE_CHANGED, self.OnPageChanged) + self.SetMenuBar(self.menubar) - self._layout() + self.SetName("ModelerFrame") self.SetMinSize((640, 300)) self.SetSize((800, 600)) - - # fix goutput's pane size - if self.goutput: - self.goutput.SetSashPosition(int(self.GetSize()[1] * 0.75)) - - def _layout(self): - """Do layout""" - sizer = wx.BoxSizer(wx.VERTICAL) - - sizer.Add(self.notebook, proportion=1, flag=wx.EXPAND) - - self.SetAutoLayout(True) - self.SetSizer(sizer) - sizer.Fit(self) - - self.Layout() - - def _addEvent(self, item): - """Add event to item""" - evthandler = ModelEvtHandler(self.statusbar, self) - evthandler.SetShape(item) - evthandler.SetPreviousHandler(item.GetEventHandler()) - item.SetEventHandler(evthandler) - - def _randomShift(self): - """Returns random value to shift layout""" - return random.randint(-self.randomness, self.randomness) - - def GetCanvas(self): - """Get canvas""" - return self.canvas - - def GetModel(self): - """Get model""" - return self.model - - def ModelChanged(self, changed=True): - """Update window title""" - self.modelChanged = changed - - if self.modelFile: - if self.modelChanged: - self.SetTitle( - self.baseTitle + " - " + os.path.basename(self.modelFile) + "*" - ) - else: - self.SetTitle(self.baseTitle + " - " + os.path.basename(self.modelFile)) - else: - self.SetTitle(self.baseTitle) - - def OnPageChanged(self, event): - """Page in notebook changed""" - page = event.GetSelection() - if page == self.notebook.GetPageIndexByName("python"): - if self.pythonPanel.IsEmpty(): - self.pythonPanel.RefreshScript() - - if self.pythonPanel.IsModified(): - self.SetStatusText( - _( - "{} script contains local modifications".format( - self.pythonPanel.body.script_type - ) - ), - 0, - ) - else: - self.SetStatusText( - _( - "{} script is up-to-date".format( - self.pythonPanel.body.script_type - ) - ), - 0, - ) - elif page == self.notebook.GetPageIndexByName("items"): - self.itemPanel.Update() - - event.Skip() - - def OnVariables(self, event): - """Switch to variables page""" - self.notebook.SetSelectionByName("variables") - - def OnRemoveItem(self, event): - """Remove shape""" - self.GetCanvas().RemoveSelected() - - def OnCanvasRefresh(self, event): - """Refresh canvas""" - self.SetStatusText(_("Redrawing model..."), 0) - self.GetCanvas().Refresh() - self.SetStatusText("", 0) - - def OnCmdRun(self, event): - """Run command""" - try: - action = self.GetModel().GetItems()[event.pid] - if hasattr(action, "task"): - action.Update(running=True) - except IndexError: - pass - - def OnCmdPrepare(self, event): - """Prepare for running command""" - if not event.userData: - return - - event.onPrepare(item=event.userData["item"], params=event.userData["params"]) - - def OnCmdDone(self, event): - """Command done (or aborted)""" - - def time_elapsed(etime): - try: - ctime = time.time() - etime - if ctime < 60: - stime = _("%d sec") % int(ctime) - else: - mtime = int(ctime / 60) - stime = _("%(min)d min %(sec)d sec") % { - "min": mtime, - "sec": int(ctime - (mtime * 60)), - } - except KeyError: - # stopped daemon - stime = _("unknown") - - return stime - - self.goutput.GetProgressBar().SetValue(0) - self.goutput.WriteCmdLog( - "({}) {} ({})".format( - str(time.ctime()), _("Command finished"), time_elapsed(event.time) - ), - notification=event.notification, - ) - - try: - action = self.GetModel().GetItems()[event.pid] - if hasattr(action, "task"): - action.Update(running=True) - if event.pid == self._gconsole.cmdThread.GetId() - 1 and self.start_time: - self.goutput.WriteCmdLog( - "({}) {} ({})".format( - str(time.ctime()), - _("Model computation finished"), - time_elapsed(self.start_time), - ), - notification=event.notification, - ) - event = wxModelDone() - wx.PostEvent(self, event) - - except IndexError: - pass - - def OnCloseWindow(self, event): - """Close window""" - if self.modelChanged and UserSettings.Get( - group="manager", key="askOnQuit", subkey="enabled" - ): - if self.modelFile: - message = _("Do you want to save changes in the model?") - else: - message = _( - "Do you want to store current model settings to model file?" - ) - - # ask user to save current settings - dlg = wx.MessageDialog( - self, - message=message, - caption=_("Quit Graphical Modeler"), - style=wx.YES_NO - | wx.YES_DEFAULT - | wx.CANCEL - | wx.ICON_QUESTION - | wx.CENTRE, - ) - ret = dlg.ShowModal() - if ret == wx.ID_YES: - if not self.modelFile: - self.OnModelSaveAs() - else: - self.WriteModelFile(self.modelFile) - elif ret == wx.ID_CANCEL: - dlg.Destroy() - return - dlg.Destroy() - - self.Destroy() - - def OnSize(self, event): - """Window resized, save to the model""" - self.ModelChanged() - event.Skip() - - def OnPreferences(self, event): - """Open preferences dialog""" - dlg = PreferencesDialog(parent=self, giface=self._giface) - dlg.CenterOnParent() - - dlg.Show() - self.canvas.Refresh() - - def OnHelp(self, event): - """Show help""" - self._giface.Help(entry="wxGUI.gmodeler") - - def OnModelProperties(self, event): - """Model properties dialog""" - dlg = PropertiesDialog(parent=self) - dlg.CentreOnParent() - properties = self.model.GetProperties() - dlg.Init(properties) - if dlg.ShowModal() == wx.ID_OK: - self.ModelChanged() - for key, value in dlg.GetValues().items(): - properties[key] = value - for action in self.model.GetItems(objType=ModelAction): - action.GetTask().set_flag("overwrite", properties["overwrite"]) - - dlg.Destroy() - - def _deleteIntermediateData(self): - """Delete intermediate data""" - rast, vect, rast3d, msg = self.model.GetIntermediateData() - if rast: - self._gconsole.RunCmd( - ["g.remove", "-f", "type=raster", "name=%s" % ",".join(rast)] - ) - if rast3d: - self._gconsole.RunCmd( - ["g.remove", "-f", "type=raster_3d", "name=%s" % ",".join(rast3d)] - ) - if vect: - self._gconsole.RunCmd( - ["g.remove", "-f", "type=vector", "name=%s" % ",".join(vect)] - ) - - self.SetStatusText( - _("%d intermediate maps deleted from current mapset") - % int(len(rast) + len(rast3d) + len(vect)) - ) - - def OnDeleteData(self, event): - """Delete intermediate data""" - rast, vect, rast3d, msg = self.model.GetIntermediateData() - - if not rast and not vect and not rast3d: - GMessage(parent=self, message=_("No intermediate data to delete.")) - return - - dlg = wx.MessageDialog( - parent=self, - message=_("Do you want to permanently delete data?%s" % msg), - caption=_("Delete intermediate data?"), - style=wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION, - ) - - ret = dlg.ShowModal() - dlg.Destroy() - if ret == wx.ID_YES: - self._deleteIntermediateData() - - def OnModelNew(self, event): - """Create new model""" - Debug.msg(4, "ModelFrame.OnModelNew():") - - # ask user to save current model - if self.modelFile and self.modelChanged: - self.OnModelSave() - elif self.modelFile is None and ( - self.model.GetNumItems() > 0 or len(self.model.GetData()) > 0 - ): - dlg = wx.MessageDialog( - self, - message=_( - "Current model is not empty. " - "Do you want to store current settings " - "to model file?" - ), - caption=_("Create new model?"), - style=wx.YES_NO | wx.YES_DEFAULT | wx.CANCEL | wx.ICON_QUESTION, - ) - ret = dlg.ShowModal() - if ret == wx.ID_YES: - self.OnModelSaveAs() - elif ret == wx.ID_CANCEL: - dlg.Destroy() - return - - dlg.Destroy() - - # delete all items - self.canvas.GetDiagram().DeleteAllShapes() - self.model.Reset() - self.canvas.Refresh() - self.itemPanel.Update() - self.variablePanel.Reset() - - # no model file loaded - self.modelFile = None - self.modelChanged = False - self.SetTitle(self.baseTitle) - - def GetModelFile(self, ext=True): - """Get model file - - :param bool ext: False to avoid extension - """ - if not self.modelFile: - return "" - if ext: - return self.modelFile - return os.path.splitext(self.modelFile)[0] - - def OnModelOpen(self, event): - """Load model from file""" - filename = "" - dlg = wx.FileDialog( - parent=self, - message=_("Choose model file"), - defaultDir=os.getcwd(), - wildcard=_("GRASS Model File (*.gxm)|*.gxm"), - ) - if dlg.ShowModal() == wx.ID_OK: - filename = dlg.GetPath() - - if not filename: - return - - Debug.msg(4, "ModelFrame.OnModelOpen(): filename=%s" % filename) - - # close current model - self.OnModelClose() - - self.LoadModelFile(filename) - - self.modelFile = filename - self.SetTitle(self.baseTitle + " - " + os.path.basename(self.modelFile)) - self.SetStatusText( - _("%(items)d items (%(actions)d actions) loaded into model") - % { - "items": self.model.GetNumItems(), - "actions": self.model.GetNumItems(actionOnly=True), - }, - 0, - ) - - def OnModelSave(self, event=None): - """Save model to file""" - if self.modelFile and self.modelChanged: - dlg = wx.MessageDialog( - self, - message=_( - "Model file <%s> already exists. " - "Do you want to overwrite this file?" - ) - % self.modelFile, - caption=_("Save model"), - style=wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION, - ) - if dlg.ShowModal() == wx.ID_NO: - dlg.Destroy() - else: - Debug.msg(4, "ModelFrame.OnModelSave(): filename=%s" % self.modelFile) - self.WriteModelFile(self.modelFile) - self.SetStatusText(_("File <%s> saved") % self.modelFile, 0) - self.SetTitle(self.baseTitle + " - " + os.path.basename(self.modelFile)) - elif not self.modelFile: - self.OnModelSaveAs() - - def OnModelSaveAs(self, event=None): - """Create model to file as""" - filename = "" - dlg = wx.FileDialog( - parent=self, - message=_("Choose file to save current model"), - defaultDir=os.getcwd(), - wildcard=_("GRASS Model File (*.gxm)|*.gxm"), - style=wx.FD_SAVE, - ) - - if dlg.ShowModal() == wx.ID_OK: - filename = dlg.GetPath() - - if not filename: - return - - # check for extension - if filename[-4:] != ".gxm": - filename += ".gxm" - - if os.path.exists(filename): - dlg = wx.MessageDialog( - parent=self, - message=_( - "Model file <%s> already exists. " - "Do you want to overwrite this file?" - ) - % filename, - caption=_("File already exists"), - style=wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION, - ) - if dlg.ShowModal() != wx.ID_YES: - dlg.Destroy() - return - - Debug.msg(4, "GMFrame.OnModelSaveAs(): filename=%s" % filename) - - self.WriteModelFile(filename) - self.modelFile = filename - self.SetTitle(self.baseTitle + " - " + os.path.basename(self.modelFile)) - self.SetStatusText(_("File <%s> saved") % self.modelFile, 0) - - def OnModelClose(self, event=None): - """Close model file""" - Debug.msg(4, "ModelFrame.OnModelClose(): file=%s" % self.modelFile) - # ask user to save current model - if self.modelFile and self.modelChanged: - self.OnModelSave() - elif self.modelFile is None and ( - self.model.GetNumItems() > 0 or len(self.model.GetData()) > 0 - ): - dlg = wx.MessageDialog( - self, - message=_( - "Current model is not empty. " - "Do you want to store current settings " - "to model file?" - ), - caption=_("Create new model?"), - style=wx.YES_NO | wx.YES_DEFAULT | wx.CANCEL | wx.ICON_QUESTION, - ) - ret = dlg.ShowModal() - if ret == wx.ID_YES: - self.OnModelSaveAs() - elif ret == wx.ID_CANCEL: - dlg.Destroy() - return - - dlg.Destroy() - - self.modelFile = None - self.SetTitle(self.baseTitle) - - self.canvas.GetDiagram().DeleteAllShapes() - self.model.Reset() - - self.canvas.Refresh() - - def OnRunModel(self, event): - """Run entire model""" - self.start_time = time.time() - self.model.Run(self._gconsole, self.OnModelDone, parent=self) - - def OnModelDone(self, event): - """Computation finished""" - self.SetStatusText("", 0) - - # restore original files - if hasattr(self.model, "fileInput"): - for finput in self.model.fileInput: - data = self.model.fileInput[finput] - if not data: - continue - - fd = open(finput, "w") - try: - fd.write(data) - finally: - fd.close() - del self.model.fileInput - - # delete intermediate data - self._deleteIntermediateData() - - # display data if required - for data in self.model.GetData(): - if not data.HasDisplay(): - continue - - # remove existing map layers first - layers = self._giface.GetLayerList().GetLayersByName(data.GetValue()) - if layers: - for layer in layers: - self._giface.GetLayerList().DeleteLayer(layer) - - # add new map layer - self._giface.GetLayerList().AddLayer( - ltype=data.GetPrompt(), - name=data.GetValue(), - checked=True, - cmd=data.GetDisplayCmd(), - ) - - def OnValidateModel(self, event, showMsg=True): - """Validate entire model""" - if self.model.GetNumItems() < 1: - GMessage(parent=self, message=_("Model is empty. Nothing to validate.")) - return - - self.SetStatusText(_("Validating model..."), 0) - errList = self.model.Validate() - self.SetStatusText("", 0) - - if errList: - GWarning( - parent=self, message=_("Model is not valid.\n\n%s") % "\n".join(errList) - ) - else: - GMessage(parent=self, message=_("Model is valid.")) - - def OnExportImage(self, event): - """Export model to image (default image)""" - xminImg = 0 - xmaxImg = 0 - yminImg = 0 - ymaxImg = 0 - # get current size of canvas - for shape in self.canvas.GetDiagram().GetShapeList(): - w, h = shape.GetBoundingBoxMax() - x = shape.GetX() - y = shape.GetY() - xmin = x - w / 2 - xmax = x + w / 2 - ymin = y - h / 2 - ymax = y + h / 2 - if xmin < xminImg: - xminImg = xmin - if xmax > xmaxImg: - xmaxImg = xmax - if ymin < yminImg: - yminImg = ymin - if ymax > ymaxImg: - ymaxImg = ymax - size = wx.Size(int(xmaxImg - xminImg) + 50, int(ymaxImg - yminImg) + 50) - bitmap = EmptyBitmap(width=size.width, height=size.height) - - filetype, ltype = GetImageHandlers(ImageFromBitmap(bitmap)) - - dlg = wx.FileDialog( - parent=self, - message=_( - "Choose a file name to save the image (no need to add extension)" - ), - defaultDir="", - defaultFile="", - wildcard=filetype, - style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT, - ) - - if dlg.ShowModal() == wx.ID_OK: - path = dlg.GetPath() - if not path: - dlg.Destroy() - return - - base, ext = os.path.splitext(path) - fileType = ltype[dlg.GetFilterIndex()]["type"] - extType = ltype[dlg.GetFilterIndex()]["ext"] - if ext != extType: - path = base + "." + extType - - dc = wx.MemoryDC(bitmap) - dc.SetBackground(wx.WHITE_BRUSH) - dc.SetBackgroundMode(wx.SOLID) - - self.canvas.GetDiagram().Clear(dc) - self.canvas.GetDiagram().Redraw(dc) - - bitmap.SaveFile(path, fileType) - self.SetStatusText(_("Model exported to <%s>") % path) - - dlg.Destroy() - - def OnExportPython(self, event=None, text=None): - """Export model to Python script""" - filename = self.pythonPanel.SaveAs(force=True) - self.SetStatusText(_("Model exported to <%s>") % filename) - - def OnDefineRelation(self, event): - """Define relation between data and action items""" - self.canvas.SetCursor(self.cursors["cross"]) - self.defineRelation = {"from": None, "to": None} - - def OnDefineLoop(self, event): - """Define new loop in the model - - .. todo:: - move to ModelCanvas? - """ - self.ModelChanged() - - width, height = self.canvas.GetSize() - loop = ModelLoop( - self, x=width / 2, y=height / 2, id=self.model.GetNumItems() + 1 - ) - self.canvas.diagram.AddShape(loop) - loop.Show(True) - - self._addEvent(loop) - self.model.AddItem(loop) - - self.canvas.Refresh() - - def OnDefineCondition(self, event): - """Define new condition in the model - - .. todo:: - move to ModelCanvas? - """ - self.ModelChanged() - - width, height = self.canvas.GetSize() - cond = ModelCondition( - self, x=width / 2, y=height / 2, id=self.model.GetNumItems() + 1 - ) - self.canvas.diagram.AddShape(cond) - cond.Show(True) - - self._addEvent(cond) - self.model.AddItem(cond) - - self.canvas.Refresh() - - def OnAddAction(self, event): - """Add action to model""" - if self.searchDialog is None: - self.searchDialog = ModelSearchDialog(parent=self, giface=self._giface) - self.searchDialog.CentreOnParent() - else: - self.searchDialog.Reset() - - if self.searchDialog.ShowModal() == wx.ID_CANCEL: - self.searchDialog.Hide() - return - - cmd = self.searchDialog.GetCmd() - self.searchDialog.Hide() - - self.ModelChanged() - - # add action to canvas - x, y = self.canvas.GetNewShapePos() - label, comment = self.searchDialog.GetLabel() - action = ModelAction( - self.model, - cmd=cmd, - x=x, - y=y, - id=self.model.GetNextId(), - label=label, - comment=comment, - ) - - overwrite = self.model.GetProperties().get("overwrite", None) - if overwrite is not None: - action.GetTask().set_flag("overwrite", overwrite) - - self.canvas.diagram.AddShape(action) - action.Show(True) - - self._addEvent(action) - self.model.AddItem(action) - - self.itemPanel.Update() - self.canvas.Refresh() - time.sleep(0.1) - - # show properties dialog - win = action.GetPropDialog() - if not win: - gmodule = GUI( - parent=self, - show=True, - giface=GraphicalModelerGrassInterface(self.model), - ) - gmodule.ParseCommand( - action.GetLog(string=False), - completed=(self.GetOptData, action, action.GetParams()), - ) - elif win and not win.IsShown(): - win.Show() - - if win: - win.Raise() - - def OnAddData(self, event): - """Add data item to model""" - # add action to canvas - width, height = self.canvas.GetSize() - data = ModelData( - self, x=width / 2 + self._randomShift(), y=height / 2 + self._randomShift() - ) - - dlg = ModelDataDialog(parent=self, shape=data) - data.SetPropDialog(dlg) - dlg.CentreOnParent() - ret = dlg.ShowModal() - dlg.Destroy() - if ret != wx.ID_OK: - return - - data.Update() - self.canvas.diagram.AddShape(data) - data.Show(True) - - self.ModelChanged() - - self._addEvent(data) - self.model.AddItem(data) - - self.canvas.Refresh() - - def OnAddComment(self, event): - """Add comment to the model""" - dlg = CustomTextEntryDialog( - parent=self, - message=_("Comment:"), - caption=_("Add comment"), - textStyle=wx.TE_MULTILINE, - textSize=(300, 75), - ) - - if dlg.ShowModal() == wx.ID_OK: - comment = dlg.GetValue() - if not comment: - GError(_("Empty comment. Nothing to add to the model."), parent=self) - else: - x, y = self.canvas.GetNewShapePos() - commentObj = ModelComment( - self.model, - x=x, - y=y, - id=self.model.GetNextId(), - label=comment, - ) - self.canvas.diagram.AddShape(commentObj) - commentObj.Show(True) - self._addEvent(commentObj) - self.model.AddItem(commentObj) - - self.canvas.Refresh() - self.ModelChanged() - - dlg.Destroy() - - def _switchPageHandler(self, event, notification): - self._switchPage(notification=notification) - event.Skip() - - def _switchPage(self, notification): - """Manages @c 'output' notebook page according to event notification.""" - if notification == Notification.HIGHLIGHT: - self.notebook.HighlightPageByName("output") - if notification == Notification.MAKE_VISIBLE: - self.notebook.SetSelectionByName("output") - if notification == Notification.RAISE_WINDOW: - self.notebook.SetSelectionByName("output") - self.SetFocus() - self.Raise() - - def OnAbout(self, event): - """Display About window""" - ShowAboutDialog(prgName=_("wxGUI Graphical Modeler"), startYear="2010") - - def GetOptData(self, dcmd, layer, params, propwin): - """Process action data""" - if params: # add data items - data_items = [] - x = layer.GetX() - y = layer.GetY() - - for p in params["params"]: - if p.get("prompt", "") not in ( - "raster", - "vector", - "raster_3d", - "dbtable", - "stds", - "strds", - "stvds", - "str3ds", - ): - continue - - # add new data item if defined or required - if p.get("value", None) or ( - p.get("age", "old") != "old" and p.get("required", "no") == "yes" - ): - data = layer.FindData(p.get("name", "")) - if data: - data.SetValue(p.get("value", "")) - data.Update() - continue - - data = self.model.FindData(p.get("value", ""), p.get("prompt", "")) - if data: - if p.get("age", "old") == "old": - rel = ModelRelation( - parent=self, - fromShape=data, - toShape=layer, - param=p.get("name", ""), - ) - else: - rel = ModelRelation( - parent=self, - fromShape=layer, - toShape=data, - param=p.get("name", ""), - ) - layer.AddRelation(rel) - data.AddRelation(rel) - self.AddLine(rel) - data.Update() - continue - - dataClass = ( - ModelDataSeries - if p.get("prompt", "").startswith("st") - else ModelDataSingle - ) - data = dataClass( - self, - value=p.get("value", ""), - prompt=p.get("prompt", ""), - x=x, - y=y, - ) - data_items.append(data) - self._addEvent(data) - self.canvas.diagram.AddShape(data) - data.Show(False) - - if p.get("age", "old") == "old": - rel = ModelRelation( - parent=self, - fromShape=data, - toShape=layer, - param=p.get("name", ""), - ) - else: - rel = ModelRelation( - parent=self, - fromShape=layer, - toShape=data, - param=p.get("name", ""), - ) - layer.AddRelation(rel) - data.AddRelation(rel) - self.AddLine(rel) - data.Update() - - # remove dead data items - if not p.get("value", ""): - data = layer.FindData(p.get("name", "")) - if data: - remList, upList = self.model.RemoveItem(data, layer) - for item in remList: - self.canvas.diagram.RemoveShape(item) - item.__del__() - - for item in upList: - item.Update() - - # valid / parameterized ? - layer.SetValid(params) - - # arrange data items - if data_items: - dc = wx.ClientDC(self.canvas) - p = 180 / (len(data_items) - 1) if len(data_items) > 1 else 0 - rx = 200 - ry = 100 - alpha = 270 * (math.pi / 180) - for data in data_items: - data.Move(dc, x + rx * math.sin(alpha), y + ry * math.cos(alpha)) - alpha += p * (math.pi / 180) - data.Show(True) - - if dcmd: - layer.SetProperties(params, propwin) - - self.canvas.Refresh() - self.SetStatusText(layer.GetLog(), 0) - - def AddLine(self, rel): - """Add connection between model objects - - :param rel: relation - """ - fromShape = rel.GetFrom() - toShape = rel.GetTo() - - rel.SetCanvas(self) - rel.SetPen(wx.BLACK_PEN) - rel.SetBrush(wx.BLACK_BRUSH) - rel.AddArrow(ogl.ARROW_ARROW) - points = rel.GetControlPoints() - rel.MakeLineControlPoints(2) - if points: - for x, y in points: - rel.InsertLineControlPoint(point=wx.RealPoint(x, y)) - - self._addEvent(rel) - try: - fromShape.AddLine(rel, toShape) - except TypeError: - pass # bug when connecting ModelCondition and ModelLoop - to be fixed - - self.canvas.diagram.AddShape(rel) - rel.Show(True) - - def LoadModelFile(self, filename): - """Load model definition stored in GRASS Model XML file (gxm)""" - try: - self.model.LoadModel(filename) - except GException as e: - GError( - parent=self, - message=_( - "Reading model file <%s> failed.\n" - "Invalid file, unable to parse XML document.\n\n%s" - ) - % (filename, e), - showTraceback=False, - ) - return - - self.modelFile = filename - self.SetTitle(self.baseTitle + " - " + os.path.basename(self.modelFile)) - - self.SetStatusText(_("Please wait, loading model..."), 0) - - # load actions - for item in self.model.GetItems(objType=ModelAction): - self._addEvent(item) - self.canvas.diagram.AddShape(item) - item.Show(True) - # relations/data - for rel in item.GetRelations(): - if rel.GetFrom() == item: - dataItem = rel.GetTo() - else: - dataItem = rel.GetFrom() - self._addEvent(dataItem) - self.canvas.diagram.AddShape(dataItem) - self.AddLine(rel) - dataItem.Show(True) - - # load loops - for item in self.model.GetItems(objType=ModelLoop): - self._addEvent(item) - self.canvas.diagram.AddShape(item) - item.Show(True) - - # connect items in the loop - self.DefineLoop(item) - - # load conditions - for item in self.model.GetItems(objType=ModelCondition): - self._addEvent(item) - self.canvas.diagram.AddShape(item) - item.Show(True) - - # connect items in the condition - self.DefineCondition(item) - - # load comments - for item in self.model.GetItems(objType=ModelComment): - self._addEvent(item) - self.canvas.diagram.AddShape(item) - item.Show(True) - - # load variables - self.variablePanel.Update() - self.itemPanel.Update() - self.SetStatusText("", 0) - - # final updates - for action in self.model.GetItems(objType=ModelAction): - action.SetValid(action.GetParams()) - action.Update() - - self.canvas.Refresh(True) - - def WriteModelFile(self, filename): - """Save model to model file, recover original file on error. - - :return: True on success - :return: False on failure - """ - self.ModelChanged(False) - tmpfile = tempfile.TemporaryFile(mode="w+") - try: - WriteModelFile(fd=tmpfile, model=self.model) - except Exception: - GError( - parent=self, message=_("Writing current settings to model file failed.") - ) - return False - - try: - mfile = open(filename, "w") - tmpfile.seek(0) - for line in tmpfile.readlines(): - mfile.write(line) - except OSError: - wx.MessageBox( - parent=self, - message=_("Unable to open file <%s> for writing.") % filename, - caption=_("Error"), - style=wx.OK | wx.ICON_ERROR | wx.CENTRE, - ) - return False - - mfile.close() - - return True - - def DefineLoop(self, loop): - """Define loop with given list of items""" - parent = loop - items = loop.GetItems(self.GetModel().GetItems()) - if not items: - return - - # remove defined relations first - for rel in loop.GetRelations(): - self.canvas.GetDiagram().RemoveShape(rel) - loop.Clear() - - for item in items: - rel = ModelRelation(parent=self, fromShape=parent, toShape=item) - dx = item.GetX() - parent.GetX() - dy = item.GetY() - parent.GetY() - loop.AddRelation(rel) - if dx != 0: - rel.SetControlPoints( - ( - (parent.GetX(), parent.GetY() + dy / 2), - (parent.GetX() + dx, parent.GetY() + dy / 2), - ) - ) - self.AddLine(rel) - parent = item - - # close loop - item = items[-1] - rel = ModelRelation(parent=self, fromShape=item, toShape=loop) - loop.AddRelation(rel) - self.AddLine(rel) - dx = (item.GetX() - loop.GetX()) + loop.GetWidth() / 2 + 50 - dy = item.GetHeight() / 2 + 50 - rel.MakeLineControlPoints(0) - rel.InsertLineControlPoint( - point=wx.RealPoint(loop.GetX() - loop.GetWidth() / 2, loop.GetY()) - ) - rel.InsertLineControlPoint( - point=wx.RealPoint(item.GetX(), item.GetY() + item.GetHeight() / 2) - ) - rel.InsertLineControlPoint(point=wx.RealPoint(item.GetX(), item.GetY() + dy)) - rel.InsertLineControlPoint( - point=wx.RealPoint(item.GetX() - dx, item.GetY() + dy) - ) - rel.InsertLineControlPoint(point=wx.RealPoint(item.GetX() - dx, loop.GetY())) - - self.canvas.Refresh() - - def DefineCondition(self, condition): - """Define if-else statement with given list of items""" - items = condition.GetItems(self.model.GetItems(objType=ModelAction)) - if not items["if"] and not items["else"]: - return - - parent = condition - - # remove defined relations first - for rel in condition.GetRelations(): - self.canvas.GetDiagram().RemoveShape(rel) - condition.Clear() - dxIf = condition.GetX() + condition.GetWidth() / 2 - dxElse = condition.GetX() - condition.GetWidth() / 2 - dy = condition.GetY() - for branch in items.keys(): - for item in items[branch]: - rel = ModelRelation(parent=self, fromShape=parent, toShape=item) - condition.AddRelation(rel) - self.AddLine(rel) - rel.MakeLineControlPoints(0) - if branch == "if": - rel.InsertLineControlPoint( - point=wx.RealPoint( - item.GetX() - item.GetWidth() / 2, item.GetY() - ) - ) - rel.InsertLineControlPoint(point=wx.RealPoint(dxIf, dy)) - else: - rel.InsertLineControlPoint(point=wx.RealPoint(dxElse, dy)) - rel.InsertLineControlPoint( - point=wx.RealPoint( - item.GetX() - item.GetWidth() / 2, item.GetY() - ) - ) - parent = item - - self.canvas.Refresh() - - -class ModelCanvas(ogl.ShapeCanvas): - """Canvas where model is drawn""" - - def __init__(self, parent): - self.parent = parent - ogl.OGLInitialize() - ogl.ShapeCanvas.__init__(self, parent) - - self.diagram = ogl.Diagram() - self.SetDiagram(self.diagram) - self.diagram.SetCanvas(self) - - self.SetScrollbars(20, 20, 2000 // 20, 2000 // 20) - - self.Bind(wx.EVT_KEY_UP, self.OnKeyUp) - self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) - - def OnKeyUp(self, event): - """Key pressed""" - kc = event.GetKeyCode() - if kc == wx.WXK_DELETE: - self.RemoveSelected() - - def OnLeftDown(self, evt): - self.SetFocus() - evt.Skip() - - def RemoveSelected(self): - """Remove selected shapes""" - self.parent.ModelChanged() - - diagram = self.GetDiagram() - shapes = [shape for shape in diagram.GetShapeList() if shape.Selected()] - self.RemoveShapes(shapes) - - def RemoveShapes(self, shapes): - """Removes shapes""" - self.parent.ModelChanged() - diagram = self.GetDiagram() - for shape in shapes: - remList, upList = self.parent.GetModel().RemoveItem(shape) - shape.Select(False) - diagram.RemoveShape(shape) - shape.__del__() - for item in remList: - diagram.RemoveShape(item) - item.__del__() - - for item in upList: - item.Update() - - self.Refresh() - - def GetNewShapePos(self, yoffset=50): - """Determine optimal position for newly added object - - :return: x,y - """ - ymax = 20 - for item in self.GetDiagram().GetShapeList(): - y = item.GetY() + item.GetBoundingBoxMin()[1] - if y > ymax: - ymax = y - - return (self.GetSize()[0] // 2, ymax + yoffset) - - def GetShapesSelected(self): - """Get list of selected shapes""" - selected = list() - diagram = self.GetDiagram() - for shape in diagram.GetShapeList(): - if shape.Selected(): - selected.append(shape) - - return selected - - -class ModelEvtHandler(ogl.ShapeEvtHandler): - """Model event handler class""" - - def __init__(self, log, frame): - ogl.ShapeEvtHandler.__init__(self) - self.log = log - self.frame = frame - self.x = self.y = None - - def OnLeftClick(self, x, y, keys=0, attachment=0): - """Left mouse button pressed -> select item & update statusbar""" - shape = self.GetShape() - canvas = shape.GetCanvas() - dc = wx.ClientDC(canvas) - - # probably does nothing, removed from wxPython 2.9 - # canvas.PrepareDC(dc) - - if hasattr(self.frame, "defineRelation"): - drel = self.frame.defineRelation - if drel["from"] is None: - drel["from"] = shape - elif drel["to"] is None: - drel["to"] = shape - rel = ModelRelation( - parent=self.frame, fromShape=drel["from"], toShape=drel["to"] - ) - dlg = ModelRelationDialog(parent=self.frame, shape=rel) - if dlg.IsValid(): - ret = dlg.ShowModal() - if ret == wx.ID_OK: - option = dlg.GetOption() - rel.SetName(option) - drel["from"].AddRelation(rel) - drel["to"].AddRelation(rel) - drel["from"].Update() - params = { - "params": [ - {"name": option, "value": drel["from"].GetValue()} - ] - } - drel["to"].MergeParams(params) - self.frame.AddLine(rel) - - dlg.Destroy() - del self.frame.defineRelation - - # select object - self._onSelectShape(shape, append=True if keys == 1 else False) - - if hasattr(shape, "GetLog"): - self.log.SetStatusText(shape.GetLog(), 0) - else: - self.log.SetStatusText("", 0) - - def OnLeftDoubleClick(self, x, y, keys=0, attachment=0): - """Left mouse button pressed (double-click) -> show properties""" - self.OnProperties() - - def OnProperties(self, event=None): - """Show properties dialog""" - self.frame.ModelChanged() - shape = self.GetShape() - if isinstance(shape, ModelAction): - gmodule = GUI( - parent=self.frame, - show=True, - giface=GraphicalModelerGrassInterface(self.frame.GetModel()), - ) - gmodule.ParseCommand( - shape.GetLog(string=False), - completed=(self.frame.GetOptData, shape, shape.GetParams()), - ) - - elif isinstance(shape, ModelData): - if shape.GetPrompt() in ( - "raster", - "vector", - "raster_3d", - "stds", - "strds", - "stvds", - "str3ds", - ): - dlg = ModelDataDialog(parent=self.frame, shape=shape) - shape.SetPropDialog(dlg) - dlg.CentreOnParent() - dlg.Show() - - elif isinstance(shape, ModelLoop): - dlg = ModelLoopDialog(parent=self.frame, shape=shape) - dlg.CentreOnParent() - if dlg.ShowModal() == wx.ID_OK: - shape.SetLabel(dlg.GetCondition()) - model = self.frame.GetModel() - ids = dlg.GetItems() - alist = list() - for aId in ids["unchecked"]: - action = model.GetItem(aId, objType=ModelAction) - if action: - action.UnSetBlock(shape) - for aId in ids["checked"]: - action = model.GetItem(aId, objType=ModelAction) - if action: - action.SetBlock(shape) - alist.append(aId) - shape.SetItems(alist) - self.frame.DefineLoop(shape) - self.frame.SetStatusText(shape.GetLog(), 0) - self.frame.GetCanvas().Refresh() - - dlg.Destroy() - - elif isinstance(shape, ModelCondition): - dlg = ModelConditionDialog(parent=self.frame, shape=shape) - dlg.CentreOnParent() - if dlg.ShowModal() == wx.ID_OK: - shape.SetLabel(dlg.GetCondition()) - model = self.frame.GetModel() - ids = dlg.GetItems() - for b in ids.keys(): - alist = list() - for aId in ids[b]["unchecked"]: - action = model.GetItem(aId, objType=ModelAction) - action.UnSetBlock(shape) - for aId in ids[b]["checked"]: - action = model.GetItem(aId, objType=ModelAction) - action.SetBlock(shape) - if action: - alist.append(aId) - shape.SetItems(alist, branch=b) - self.frame.DefineCondition(shape) - self.frame.GetCanvas().Refresh() - - dlg.Destroy() - - def OnBeginDragLeft(self, x, y, keys=0, attachment=0): - """Drag shape (beginning)""" - self.frame.ModelChanged() - if self._previousHandler: - self._previousHandler.OnBeginDragLeft(x, y, keys, attachment) - - def OnEndDragLeft(self, x, y, keys=0, attachment=0): - """Drag shape (end)""" - if self._previousHandler: - self._previousHandler.OnEndDragLeft(x, y, keys, attachment) - - shape = self.GetShape() - if isinstance(shape, ModelLoop): - self.frame.DefineLoop(shape) - elif isinstance(shape, ModelCondition): - self.frame.DefineCondition(shape) - - for mo in shape.GetBlock(): - if isinstance(mo, ModelLoop): - self.frame.DefineLoop(mo) - elif isinstance(mo, ModelCondition): - self.frame.DefineCondition(mo) - - shape = self.GetShape() - canvas = shape.GetCanvas() - canvas.Refresh() - - def OnEndSize(self, x, y): - """Resize shape""" - self.frame.ModelChanged() - if self._previousHandler: - self._previousHandler.OnEndSize(x, y) - - def OnRightClick(self, x, y, keys=0, attachment=0): - """Right click -> pop-up menu""" - if not hasattr(self, "popupID"): - self.popupID = dict() - for key in ( - "remove", - "enable", - "addPoint", - "delPoint", - "intermediate", - "display", - "props", - "id", - "label", - "comment", - ): - self.popupID[key] = NewId() - - # record coordinates - self.x = x - self.y = y - - # select object - shape = self.GetShape() - self._onSelectShape(shape) - - popupMenu = Menu() - popupMenu.Append(self.popupID["remove"], _("Remove")) - self.frame.Bind(wx.EVT_MENU, self.OnRemove, id=self.popupID["remove"]) - if isinstance(shape, ModelAction) or isinstance(shape, ModelLoop): - if shape.IsEnabled(): - popupMenu.Append(self.popupID["enable"], _("Disable")) - self.frame.Bind(wx.EVT_MENU, self.OnDisable, id=self.popupID["enable"]) - else: - popupMenu.Append(self.popupID["enable"], _("Enable")) - self.frame.Bind(wx.EVT_MENU, self.OnEnable, id=self.popupID["enable"]) - if isinstance(shape, ModelAction) or isinstance(shape, ModelComment): - popupMenu.AppendSeparator() - if isinstance(shape, ModelAction): - popupMenu.Append(self.popupID["label"], _("Set label")) - self.frame.Bind(wx.EVT_MENU, self.OnSetLabel, id=self.popupID["label"]) - if isinstance(shape, ModelAction) or isinstance(shape, ModelComment): - popupMenu.Append(self.popupID["comment"], _("Set comment")) - self.frame.Bind(wx.EVT_MENU, self.OnSetComment, id=self.popupID["comment"]) - - if isinstance(shape, ModelRelation): - popupMenu.AppendSeparator() - popupMenu.Append(self.popupID["addPoint"], _("Add control point")) - self.frame.Bind(wx.EVT_MENU, self.OnAddPoint, id=self.popupID["addPoint"]) - popupMenu.Append(self.popupID["delPoint"], _("Remove control point")) - self.frame.Bind( - wx.EVT_MENU, self.OnRemovePoint, id=self.popupID["delPoint"] - ) - if len(shape.GetLineControlPoints()) == 2: - popupMenu.Enable(self.popupID["delPoint"], False) - - if isinstance(shape, ModelData): - popupMenu.AppendSeparator() - if ( - "@" not in shape.GetValue() - and len(self.GetShape().GetRelations("from")) > 0 - ): - popupMenu.Append( - self.popupID["intermediate"], _("Intermediate"), kind=wx.ITEM_CHECK - ) - if self.GetShape().IsIntermediate(): - popupMenu.Check(self.popupID["intermediate"], True) - - self.frame.Bind( - wx.EVT_MENU, self.OnIntermediate, id=self.popupID["intermediate"] - ) - - if self.frame._giface.GetMapDisplay(): - popupMenu.Append( - self.popupID["display"], _("Display"), kind=wx.ITEM_CHECK - ) - if self.GetShape().HasDisplay(): - popupMenu.Check(self.popupID["display"], True) - - self.frame.Bind( - wx.EVT_MENU, self.OnHasDisplay, id=self.popupID["display"] - ) - - if self.GetShape().IsIntermediate(): - popupMenu.Enable(self.popupID["display"], False) - - if ( - isinstance(shape, ModelData) - or isinstance(shape, ModelAction) - or isinstance(shape, ModelLoop) - ): - popupMenu.AppendSeparator() - popupMenu.Append(self.popupID["props"], _("Properties")) - self.frame.Bind(wx.EVT_MENU, self.OnProperties, id=self.popupID["props"]) - - self.frame.PopupMenu(popupMenu) - popupMenu.Destroy() - - def OnDisable(self, event): - """Disable action""" - self._onEnable(False) - - def OnEnable(self, event): - """Disable action""" - self._onEnable(True) - - def _onEnable(self, enable): - shape = self.GetShape() - shape.Enable(enable) - self.frame.ModelChanged() - self.frame.canvas.Refresh() - - def OnSetLabel(self, event): - shape = self.GetShape() - dlg = wxTextEntryDialog( - parent=self.frame, - message=_("Label:"), - caption=_("Set label"), - value=shape.GetLabel(), - ) - if dlg.ShowModal() == wx.ID_OK: - label = dlg.GetValue() - shape.SetLabel(label) - self.frame.ModelChanged() - self.frame.itemPanel.Update() - self.frame.canvas.Refresh() - dlg.Destroy() - - def OnSetComment(self, event): - shape = self.GetShape() - dlg = CustomTextEntryDialog( - parent=self.frame, - message=_("Comment:"), - caption=_("Set comment"), - defaultValue=shape.GetComment(), - textStyle=wx.TE_MULTILINE, - textSize=(300, 75), - ) - if dlg.ShowModal() == wx.ID_OK: - comment = dlg.GetValue() - shape.SetComment(comment) - self.frame.ModelChanged() - self.frame.canvas.Refresh() - dlg.Destroy() - - def _onSelectShape(self, shape, append=False): - canvas = shape.GetCanvas() - dc = wx.ClientDC(canvas) - - if shape.Selected(): - shape.Select(False, dc) - else: - redraw = False - shapeList = canvas.GetDiagram().GetShapeList() - toUnselect = list() - - if not append: - for s in shapeList: - if s.Selected(): - toUnselect.append(s) - - shape.Select(True, dc) - - for s in toUnselect: - s.Select(False, dc) - - canvas.Refresh(False) - - def OnAddPoint(self, event): - """Add control point""" - shape = self.GetShape() - shape.InsertLineControlPoint(point=wx.RealPoint(self.x, self.y)) - shape.ResetShapes() - shape.Select(True) - self.frame.ModelChanged() - self.frame.canvas.Refresh() - - def OnRemovePoint(self, event): - """Remove control point""" - shape = self.GetShape() - shape.DeleteLineControlPoint() - shape.Select(False) - shape.Select(True) - self.frame.ModelChanged() - self.frame.canvas.Refresh() - - def OnIntermediate(self, event): - """Mark data as intermediate""" - self.frame.ModelChanged() - shape = self.GetShape() - shape.SetIntermediate(event.IsChecked()) - self.frame.canvas.Refresh() - - def OnHasDisplay(self, event): - """Mark data to be displayed""" - self.frame.ModelChanged() - shape = self.GetShape() - shape.SetHasDisplay(event.IsChecked()) - self.frame.canvas.Refresh() - - try: - if event.IsChecked(): - # add map layer to display - self.frame._giface.GetLayerList().AddLayer( - ltype=shape.GetPrompt(), - name=shape.GetValue(), - checked=True, - cmd=shape.GetDisplayCmd(), - ) - else: - # remove map layer(s) from display - layers = self.frame._giface.GetLayerList().GetLayersByName( - shape.GetValue() - ) - for layer in layers: - self.frame._giface.GetLayerList().DeleteLayer(layer) - - except GException as e: - GError(parent=self, message="{}".format(e)) - - def OnRemove(self, event): - """Remove shape""" - self.frame.GetCanvas().RemoveShapes([self.GetShape()]) - self.frame.itemPanel.Update() - - -class VariablePanel(wx.Panel): - def __init__(self, parent, id=wx.ID_ANY, **kwargs): - """Manage model variables panel""" - self.parent = parent - - wx.Panel.__init__(self, parent=parent, id=id, **kwargs) - - self.listBox = StaticBox( - parent=self, - id=wx.ID_ANY, - label=" %s " % _("List of variables - right-click to delete"), - ) - - self.list = VariableListCtrl( - parent=self, - columns=[_("Name"), _("Data type"), _("Default value"), _("Description")], - frame=self.parent, - ) - - # add new category - self.addBox = StaticBox( - parent=self, id=wx.ID_ANY, label=" %s " % _("Add new variable") - ) - self.name = TextCtrl(parent=self, id=wx.ID_ANY) - wx.CallAfter(self.name.SetFocus) - self.type = wx.Choice( - parent=self, - id=wx.ID_ANY, - choices=[ - _("integer"), - _("float"), - _("string"), - _("raster"), - _("vector"), - _("region"), - _("mapset"), - _("file"), - _("dir"), - ], - ) - self.type.SetSelection(2) # string - self.value = TextCtrl(parent=self, id=wx.ID_ANY) - self.desc = TextCtrl(parent=self, id=wx.ID_ANY) - - # buttons - self.btnAdd = Button(parent=self, id=wx.ID_ADD) - self.btnAdd.SetToolTip(_("Add new variable to the model")) - self.btnAdd.Enable(False) - - # bindings - self.name.Bind(wx.EVT_TEXT, self.OnText) - self.value.Bind(wx.EVT_TEXT, self.OnText) - self.desc.Bind(wx.EVT_TEXT, self.OnText) - self.btnAdd.Bind(wx.EVT_BUTTON, self.OnAdd) - - self._layout() - - def _layout(self): - """Layout dialog""" - listSizer = wx.StaticBoxSizer(self.listBox, wx.VERTICAL) - listSizer.Add(self.list, proportion=1, flag=wx.EXPAND) - - addSizer = wx.StaticBoxSizer(self.addBox, wx.VERTICAL) - gridSizer = wx.GridBagSizer(hgap=5, vgap=5) - gridSizer.Add( - StaticText(parent=self, id=wx.ID_ANY, label="%s:" % _("Name")), - flag=wx.ALIGN_CENTER_VERTICAL, - pos=(0, 0), - ) - gridSizer.Add(self.name, pos=(0, 1), flag=wx.EXPAND) - gridSizer.Add( - StaticText(parent=self, id=wx.ID_ANY, label="%s:" % _("Data type")), - flag=wx.ALIGN_CENTER_VERTICAL, - pos=(0, 2), - ) - gridSizer.Add(self.type, pos=(0, 3)) - gridSizer.Add( - StaticText(parent=self, id=wx.ID_ANY, label="%s:" % _("Default value")), - flag=wx.ALIGN_CENTER_VERTICAL, - pos=(1, 0), - ) - gridSizer.Add(self.value, pos=(1, 1), span=(1, 3), flag=wx.EXPAND) - gridSizer.Add( - StaticText(parent=self, id=wx.ID_ANY, label="%s:" % _("Description")), - flag=wx.ALIGN_CENTER_VERTICAL, - pos=(2, 0), - ) - gridSizer.Add(self.desc, pos=(2, 1), span=(1, 3), flag=wx.EXPAND) - gridSizer.AddGrowableCol(1) - addSizer.Add(gridSizer, flag=wx.EXPAND) - addSizer.Add(self.btnAdd, proportion=0, flag=wx.TOP | wx.ALIGN_RIGHT, border=5) - - mainSizer = wx.BoxSizer(wx.VERTICAL) - mainSizer.Add(listSizer, proportion=1, flag=wx.EXPAND | wx.ALL, border=5) - mainSizer.Add( - addSizer, - proportion=0, - flag=wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, - border=5, - ) - - self.SetSizer(mainSizer) - mainSizer.Fit(self) - - def OnText(self, event): - """Text entered""" - if self.name.GetValue(): - self.btnAdd.Enable() - else: - self.btnAdd.Enable(False) - - def OnAdd(self, event): - """Add new variable to the list""" - msg = self.list.Append( - self.name.GetValue(), - self.type.GetStringSelection(), - self.value.GetValue(), - self.desc.GetValue(), - ) - self.name.SetValue("") - self.name.SetFocus() - - if msg: - GError(parent=self, message=msg) - else: - self.type.SetSelection(2) # string - self.value.SetValue("") - self.desc.SetValue("") - self.UpdateModelVariables() - - def UpdateModelVariables(self): - """Update model variables""" - variables = dict() - for values in self.list.GetData().values(): - name = values[0] - variables[name] = {"type": str(values[1])} - if values[2]: - variables[name]["value"] = values[2] - if values[3]: - variables[name]["description"] = values[3] - - self.parent.GetModel().SetVariables(variables) - self.parent.ModelChanged() - - def Update(self): - """Reload list of variables""" - self.list.OnReload(None) - - def Reset(self): - """Remove all variables""" - self.list.DeleteAllItems() - self.parent.GetModel().SetVariables([]) - - -class ItemPanel(wx.Panel): - def __init__(self, parent, id=wx.ID_ANY, **kwargs): - """Manage model items""" - self.parent = parent - - wx.Panel.__init__(self, parent=parent, id=id, **kwargs) - - self.listBox = StaticBox( - parent=self, - id=wx.ID_ANY, - label=" %s " % _("List of items - right-click to delete"), - ) - - self.list = ItemListCtrl( - parent=self, - columns=[_("Label"), _("In loop"), _("Parameterized"), _("Command")], - columnsNotEditable=[1, 2, 3], - frame=self.parent, - ) - - self.btnMoveUp = Button(parent=self, id=wx.ID_UP) - self.btnMoveDown = Button(parent=self, id=wx.ID_DOWN) - self.btnRefresh = Button(parent=self, id=wx.ID_REFRESH) - - self.btnMoveUp.Bind(wx.EVT_BUTTON, self.OnMoveItemsUp) - self.btnMoveDown.Bind(wx.EVT_BUTTON, self.OnMoveItemsDown) - self.btnRefresh.Bind(wx.EVT_BUTTON, self.list.OnReload) - - self._layout() - - def _layout(self): - """Layout dialog""" - listSizer = wx.StaticBoxSizer(self.listBox, wx.VERTICAL) - listSizer.Add(self.list, proportion=1, flag=wx.EXPAND) - - manageSizer = wx.BoxSizer(wx.VERTICAL) - manageSizer.Add(self.btnMoveUp, border=5, flag=wx.ALL) - manageSizer.Add(self.btnMoveDown, border=5, flag=wx.LEFT | wx.RIGHT | wx.BOTTOM) - manageSizer.Add(self.btnRefresh, border=5, flag=wx.LEFT | wx.RIGHT) - - mainSizer = wx.BoxSizer(wx.HORIZONTAL) - mainSizer.Add(listSizer, proportion=1, flag=wx.EXPAND | wx.ALL, border=3) - mainSizer.Add(manageSizer, proportion=0, flag=wx.EXPAND | wx.ALL, border=3) - - self.SetSizer(mainSizer) - mainSizer.Fit(self) - - def Update(self): - """Reload list of variables""" - self.list.OnReload(None) - - def _getSelectedItems(self): - """Get list of selected items, indices start at 0""" - items = [] - current = -1 - while True: - next = self.list.GetNextSelected(current) - if next == -1: - break - items.append(next) - current = next - - if not items: - GMessage(_("No items to selected."), parent=self) - - return items - - def OnMoveItemsUp(self, event): - """Item moved up, update action ids""" - items = self._getSelectedItems() - if not items: - return - self.list.MoveItems(items, up=True) - self.parent.GetCanvas().Refresh() - self.parent.ModelChanged() - - def OnMoveItemsDown(self, event): - """Item moved up, update action ids""" - items = self._getSelectedItems() - if not items: - return - self.list.MoveItems(items, up=False) - self.parent.GetCanvas().Refresh() - self.parent.ModelChanged() - - -class PythonPanel(wx.Panel): - """Model as a Python script of choice.""" - - def __init__(self, parent, id=wx.ID_ANY, **kwargs): - """Initialize the panel.""" - self.parent = parent - - wx.Panel.__init__(self, parent=parent, id=id, **kwargs) - - # variable for a temp file to run Python scripts - self.filename = None - # default values of variables that will be changed if the desired - # script type is changed - self.write_object = WritePythonFile - - self.bodyBox = StaticBox( - parent=self, id=wx.ID_ANY, label=" %s " % _("Python script") - ) - self.body = PyStc(parent=self, statusbar=self.parent.GetStatusBar()) - if IsDark(): - SetDarkMode(self.body) - - self.btnRun = Button(parent=self, id=wx.ID_ANY, label=_("&Run")) - self.btnRun.SetToolTip(_("Run script")) - self.Bind(wx.EVT_BUTTON, self.OnRun, self.btnRun) - self.btnSaveAs = Button(parent=self, id=wx.ID_SAVEAS) - self.btnSaveAs.SetToolTip(_("Save the script to a file")) - self.Bind(wx.EVT_BUTTON, self.OnSaveAs, self.btnSaveAs) - self.btnRefresh = Button(parent=self, id=wx.ID_REFRESH) - self.btnRefresh.SetToolTip( - _( - "Refresh the script based on the model.\n" - "It will discard all local changes." - ) - ) - self.script_type_box = wx.Choice( - parent=self, - id=wx.ID_ANY, - choices=[ - _("Python"), - _("PyWPS"), - ], - ) - self.script_type_box.SetSelection(0) # Python - self.Bind(wx.EVT_BUTTON, self.OnRefresh, self.btnRefresh) - self.Bind( - wx.EVT_CHOICE, - self.OnChangeScriptType, - self.script_type_box, - ) - - self._layout() - - def _layout(self): - sizer = wx.BoxSizer(wx.VERTICAL) - bodySizer = wx.StaticBoxSizer(self.bodyBox, wx.HORIZONTAL) - btnSizer = wx.BoxSizer(wx.HORIZONTAL) - - bodySizer.Add(self.body, proportion=1, flag=wx.EXPAND | wx.ALL, border=3) - - btnSizer.Add( - StaticText( - parent=self, id=wx.ID_ANY, label="%s:" % _("Python script type") - ), - flag=wx.ALIGN_CENTER_VERTICAL, - ) - btnSizer.Add(self.script_type_box, proportion=0, flag=wx.RIGHT, border=5) - btnSizer.AddStretchSpacer() - btnSizer.Add(self.btnRefresh, proportion=0, flag=wx.LEFT | wx.RIGHT, border=5) - btnSizer.Add(self.btnSaveAs, proportion=0, flag=wx.RIGHT, border=5) - btnSizer.Add(self.btnRun, proportion=0, flag=wx.RIGHT, border=5) - - sizer.Add(bodySizer, proportion=1, flag=wx.EXPAND | wx.ALL, border=3) - sizer.Add(btnSizer, proportion=0, flag=wx.EXPAND | wx.ALL, border=3) - - sizer.Fit(self) - sizer.SetSizeHints(self) - self.SetSizer(sizer) - - def RefreshScript(self): - """Refresh the script. - - :return: True on refresh - :return: False script hasn't been updated - """ - if len(self.parent.GetModel().GetItems()) == 0: - # no need to fully parse an empty script - self.body.SetText("") - return True - - if self.body.modified: - dlg = wx.MessageDialog( - self, - message=_( - "{} script is locally modified. " - "Refresh will discard all changes. " - "Do you really want to continue?".format(self.body.script_type) - ), - caption=_("Update"), - style=wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION | wx.CENTRE, - ) - ret = dlg.ShowModal() - dlg.Destroy() - if ret == wx.ID_NO: - return False - - fd = tempfile.TemporaryFile(mode="r+") - grassAPI = UserSettings.Get(group="modeler", key="grassAPI", subkey="selection") - self.write_object( - fd, - self.parent.GetModel(), - grassAPI="script" if grassAPI == 0 else "pygrass", - ) - - fd.seek(0) - self.body.SetText(fd.read()) - fd.close() - - self.body.modified = False - - return True - - def SaveAs(self, force=False): - """Save the script to a file. - - :return: filename - """ - filename = "" - dlg = wx.FileDialog( - parent=self, - message=_("Choose file to save"), - defaultFile=os.path.basename(self.parent.GetModelFile(ext=False)), - defaultDir=os.getcwd(), - wildcard=_("Python script (*.py)|*.py"), - style=wx.FD_SAVE, - ) - - if dlg.ShowModal() == wx.ID_OK: - filename = dlg.GetPath() - - if not filename: - return "" - - # check for extension - if filename[-3:] != ".py": - filename += ".py" - - if os.path.exists(filename): - dlg = wx.MessageDialog( - self, - message=_( - "File <%s> already exists. " "Do you want to overwrite this file?" - ) - % filename, - caption=_("Save file"), - style=wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION, - ) - if dlg.ShowModal() == wx.ID_NO: - dlg.Destroy() - return "" - - dlg.Destroy() - - fd = open(filename, "w") - try: - if force: - self.write_object(fd, self.parent.GetModel()) - else: - fd.write(self.body.GetText()) - finally: - fd.close() - - # executable file - os.chmod(filename, stat.S_IRWXU | stat.S_IWUSR) - - return filename - - def OnRun(self, event): - """Run Python script""" - self.filename = grass.tempfile() - try: - fd = open(self.filename, "w") - fd.write(self.body.GetText()) - except OSError as e: - GError(_("Unable to launch Python script. %s") % e, parent=self) - return - finally: - fd.close() - mode = stat.S_IMODE(os.lstat(self.filename)[stat.ST_MODE]) - os.chmod(self.filename, mode | stat.S_IXUSR) - - for item in self.parent.GetModel().GetItems(): - if ( - len(item.GetParameterizedParams()["params"]) - + len(item.GetParameterizedParams()["flags"]) - > 0 - ): - self.parent._gconsole.RunCmd( - [fd.name, "--ui"], skipInterface=False, onDone=self.OnDone - ) - break - else: - self.parent._gconsole.RunCmd( - [fd.name], skipInterface=True, onDone=self.OnDone - ) - - event.Skip() - - def OnDone(self, event): - """Python script finished""" - try_remove(self.filename) - self.filename = None - - def OnChangeScriptType(self, event): - new_script_type = self.script_type_box.GetStringSelection() - if new_script_type == "Python": - self.write_object = WritePythonFile - elif new_script_type == "PyWPS": - self.write_object = WritePyWPSFile - - if self.RefreshScript(): - self.body.script_type = new_script_type - self.parent.SetStatusText( - _("{} script is up-to-date".format(self.body.script_type)), - 0, - ) - - self.script_type_box.SetStringSelection(self.body.script_type) - - if self.body.script_type == "Python": - self.write_object = WritePythonFile - self.btnRun.Enable() - self.btnRun.SetToolTip(_("Run script")) - elif self.body.script_type == "PyWPS": - self.write_object = WritePyWPSFile - self.btnRun.Disable() - self.btnRun.SetToolTip( - _("Run script - enabled only for basic Python scripts") - ) - - def OnRefresh(self, event): - """Refresh the script.""" - if self.RefreshScript(): - self.parent.SetStatusText( - _("{} script is up-to-date".format(self.body.script_type)), - 0, - ) - event.Skip() - - def OnSaveAs(self, event): - """Save the script to a file.""" - self.SaveAs(force=False) - event.Skip() - - def IsModified(self): - """Check if the script has been modified.""" - return self.body.modified - - def IsEmpty(self): - """Check if the script is empty.""" - return len(self.body.GetText()) == 0 diff --git a/gui/wxpython/gmodeler/g.gui.gmodeler.py b/gui/wxpython/gmodeler/g.gui.gmodeler.py index 2d6312d084b..c2f24752583 100755 --- a/gui/wxpython/gmodeler/g.gui.gmodeler.py +++ b/gui/wxpython/gmodeler/g.gui.gmodeler.py @@ -4,7 +4,7 @@ # MODULE: g.gui.gmodeler # AUTHOR(S): Martin Landa # PURPOSE: Graphical Modeler to create, edit, and manage models -# COPYRIGHT: (C) 2010-2012 by Martin Landa, and the GRASS Development Team +# COPYRIGHT: (C) 2010-2023 by Martin Landa, and the GRASS Development Team # # This program is free software; you can 1redistribute it and/or # modify it under the terms of the GNU General Public License as @@ -47,16 +47,16 @@ def main(): set_gui_path() from core.giface import StandaloneGrassInterface - from gmodeler.frame import ModelFrame + from gmodeler.frame import ModelerFrame app = wx.App() - frame = ModelFrame( + frame = ModelerFrame( parent=None, giface=StandaloneGrassInterface(), title=_("Graphical Modeler - GRASS GIS"), ) if options["file"]: - frame.LoadModelFile(options["file"]) + frame.panel.LoadModelFile(options["file"]) frame.Show() app.MainLoop() diff --git a/gui/wxpython/gmodeler/model.py b/gui/wxpython/gmodeler/model.py index 7188606c6e2..6b4e84a2681 100644 --- a/gui/wxpython/gmodeler/model.py +++ b/gui/wxpython/gmodeler/model.py @@ -55,6 +55,7 @@ GetDefaultEncoding, ) from core.settings import UserSettings +from core.giface import StandaloneGrassInterface from gui_core.forms import GUI, CmdPanel from gui_core.widgets import GNotebook from gui_core.wrap import Button, IsDark @@ -314,8 +315,6 @@ def LoadModel(self, filename): Raise exception on error. """ - dtdFilename = os.path.join(globalvar.WXGUIDIR, "xml", "grass-gxm.dtd") - # parse workspace file try: gxmXml = ProcessModelFile(etree.parse(filename)) @@ -324,10 +323,11 @@ def LoadModel(self, filename): if self.canvas: win = self.canvas.parent - if gxmXml.pos: - win.SetPosition(gxmXml.pos) - if gxmXml.size: - win.SetSize(gxmXml.size) + if isinstance(win._giface, StandaloneGrassInterface): + if gxmXml.pos: + win.SetPosition(gxmXml.pos) + if gxmXml.size: + win.SetSize(gxmXml.size) # load properties self.properties = gxmXml.properties @@ -1143,7 +1143,7 @@ def SetLabel(self, label=None): else: try: label = self.task.get_cmd(ignoreErrors=True)[0] - except: + except IndexError: label = _("unknown") idx = self.GetId() @@ -1997,7 +1997,7 @@ def __init__(self, tree): if self.root is not None: tagName = self.root.tag else: - tabName = _("empty") + tagName = _("empty") raise GException(_("Details: unsupported tag name '{0}'.").format(tagName)) # list of actions, data @@ -2135,7 +2135,7 @@ def _getDim(self, node): posVal = list(map(int, posAttr.split(","))) try: pos = (posVal[0], posVal[1]) - except: + except IndexError: pos = None sizeAttr = node.get("size", None) @@ -2143,7 +2143,7 @@ def _getDim(self, node): sizeVal = list(map(int, sizeAttr.split(","))) try: size = (sizeVal[0], sizeVal[1]) - except: + except IndexError: size = None return pos, size diff --git a/gui/wxpython/gmodeler/panels.py b/gui/wxpython/gmodeler/panels.py new file mode 100644 index 00000000000..1e650ffaa1c --- /dev/null +++ b/gui/wxpython/gmodeler/panels.py @@ -0,0 +1,1829 @@ +""" +@package gmodeler.frame + +@brief wxGUI Graphical Modeler for creating, editing, and managing models + +Classes: + - panels::ModelerPanel + - panels::VariablePanel + - panels::ItemPanel + - panels::PythonPanel + +(C) 2010-2023 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 +@author Python exports Ondrej Pesek +""" + +import os +import time +import stat +import tempfile +import random +import six +import math + +import wx + +from wx.lib import ogl +from core import globalvar + +if globalvar.wxPythonPhoenix: + try: + import agw.flatnotebook as FN + except ImportError: # if it's not there locally, try the wxPython lib. + import wx.lib.agw.flatnotebook as FN +else: + import wx.lib.flatnotebook as FN +from wx.lib.newevent import NewEvent + +from core.gconsole import GConsole, EVT_CMD_RUN, EVT_CMD_DONE, EVT_CMD_PREPARE +from core.debug import Debug +from core.gcmd import GMessage, GException, GWarning, GError +from core.settings import UserSettings +from core.giface import Notification + +from gui_core.widgets import GNotebook +from gui_core.goutput import GConsoleWindow +from gui_core.dialogs import GetImageHandlers +from gui_core.dialogs import TextEntryDialog as CustomTextEntryDialog +from gui_core.ghelp import ShowAboutDialog +from gui_core.forms import GUI +from gui_core.pystc import PyStc, SetDarkMode +from gui_core.wrap import ( + Button, + EmptyBitmap, + ImageFromBitmap, + StaticBox, + StaticText, + StockCursor, + TextCtrl, + IsDark, +) +from main_window.page import MainPageBase +from gmodeler.giface import GraphicalModelerGrassInterface +from gmodeler.model import ( + Model, + ModelAction, + ModelData, + ModelRelation, + ModelLoop, + ModelCondition, + ModelComment, + WriteModelFile, + ModelDataSeries, + ModelDataSingle, + WritePythonFile, + WritePyWPSFile, +) +from gmodeler.dialogs import ( + ModelDataDialog, + ModelSearchDialog, + VariableListCtrl, + ItemListCtrl, +) +from gmodeler.canvas import ModelCanvas, ModelEvtHandler +from gmodeler.toolbars import ModelerToolbar +from gmodeler.preferences import PreferencesDialog, PropertiesDialog + +from grass.script.utils import try_remove +from grass.script import core as grass + +wxModelDone, EVT_MODEL_DONE = NewEvent() + + +class ModelerPanel(wx.Panel, MainPageBase): + def __init__( + self, + parent, + giface, + id=wx.ID_ANY, + title=_("Graphical Modeler"), + statusbar=None, + dockable=False, + **kwargs, + ): + """Graphical modeler main panel + :param parent: parent window + :param giface: GRASS interface + :param id: window id + :param title: window title + + :param kwargs: wx.Panel' arguments + """ + self.parent = parent + self._giface = giface + self.statusbar = statusbar + + self.searchDialog = None # module search dialog + self.baseTitle = title + self.modelFile = None # loaded model + self.start_time = None + self.modelChanged = False + self.randomness = 40 # random layout + + self.cursors = { + "default": StockCursor(wx.CURSOR_ARROW), + "cross": StockCursor(wx.CURSOR_CROSS), + } + + wx.Panel.__init__(self, parent=parent, id=id, **kwargs) + MainPageBase.__init__(self, dockable) + + self.SetName("Modeler") + + self.toolbar = ModelerToolbar(parent=self) + + self.notebook = GNotebook(parent=self, style=globalvar.FNPageDStyle) + + self.canvas = ModelCanvas(self) + self.canvas.SetBackgroundColour( + wx.SystemSettings().GetColour(wx.SYS_COLOUR_WINDOW) + ) + self.canvas.SetCursor(self.cursors["default"]) + + self.model = Model(self.canvas) + + self.variablePanel = VariablePanel(parent=self) + + self.itemPanel = ItemPanel(parent=self) + + self.pythonPanel = PythonPanel(parent=self) + + self._gconsole = GConsole(guiparent=self, giface=giface) + self.goutput = GConsoleWindow( + parent=self, giface=giface, gconsole=self._gconsole + ) + self.goutput.showNotification.connect( + lambda message: self.SetStatusText(message) + ) + + # here events are binded twice + self._gconsole.Bind( + EVT_CMD_RUN, + lambda event: self._switchPageHandler( + event=event, notification=Notification.MAKE_VISIBLE + ), + ) + self._gconsole.Bind( + EVT_CMD_DONE, + lambda event: self._switchPageHandler( + event=event, notification=Notification.RAISE_WINDOW + ), + ) + self.Bind(EVT_CMD_RUN, self.OnCmdRun) + # rewrite default method to avoid hiding progress bar + self._gconsole.Bind(EVT_CMD_DONE, self.OnCmdDone) + self.Bind(EVT_CMD_PREPARE, self.OnCmdPrepare) + self.Bind(EVT_MODEL_DONE, self.OnModelDone) + + self.notebook.AddPage(page=self.canvas, text=_("Model"), name="model") + self.notebook.AddPage(page=self.itemPanel, text=_("Items"), name="items") + self.notebook.AddPage( + page=self.variablePanel, text=_("Variables"), name="variables" + ) + self.notebook.AddPage( + page=self.pythonPanel, text=_("Python editor"), name="python" + ) + self.notebook.AddPage( + page=self.goutput, text=_("Command output"), name="output" + ) + wx.CallAfter(self.notebook.SetSelectionByName, "model") + wx.CallAfter(self.ModelChanged, False) + + self.Bind(wx.EVT_SIZE, self.OnSize) + self.notebook.Bind(FN.EVT_FLATNOTEBOOK_PAGE_CHANGED, self.OnPageChanged) + + self._layout() + + # fix goutput's pane size + if self.goutput: + self.goutput.SetSashPosition(int(self.GetSize()[1] * 0.75)) + + def _layout(self): + """Do layout""" + sizer = wx.BoxSizer(wx.VERTICAL) + + sizer.Add(self.toolbar, proportion=0, flag=wx.EXPAND) + sizer.Add(self.notebook, proportion=1, flag=wx.EXPAND) + + self.SetAutoLayout(True) + self.SetSizer(sizer) + sizer.Fit(self) + + self.Layout() + + def _addEvent(self, item): + """Add event to item""" + evthandler = ModelEvtHandler(self.statusbar, self) + evthandler.SetShape(item) + evthandler.SetPreviousHandler(item.GetEventHandler()) + item.SetEventHandler(evthandler) + + def _randomShift(self): + """Returns random value to shift layout""" + return random.randint(-self.randomness, self.randomness) + + def SetStatusText(self, *args): + self.statusbar.SetStatusText(*args) + + def GetStatusBar(self): + """Get statusbar""" + return self.statusbar + + def GetCanvas(self): + """Get canvas""" + return self.canvas + + def GetModel(self): + """Get model""" + return self.model + + def ModelChanged(self, changed=True): + """Update window title""" + self.modelChanged = changed + + if self.modelFile: + if self.modelChanged: + self.RenamePage( + self.baseTitle + " - " + os.path.basename(self.modelFile) + "*" + ) + else: + self.RenamePage( + self.baseTitle + " - " + os.path.basename(self.modelFile) + ) + else: + self.RenamePage(self.baseTitle) + + def OnPageChanged(self, event): + """Page in notebook changed""" + page = event.GetSelection() + if page == self.notebook.GetPageIndexByName("python"): + if self.pythonPanel.IsEmpty(): + self.pythonPanel.RefreshScript() + + if self.pythonPanel.IsModified(): + self.SetStatusText( + _( + "{} script contains local modifications".format( + self.pythonPanel.body.script_type + ) + ), + 0, + ) + else: + self.SetStatusText( + _( + "{} script is up-to-date".format( + self.pythonPanel.body.script_type + ) + ), + 0, + ) + elif page == self.notebook.GetPageIndexByName("items"): + self.itemPanel.Update() + + event.Skip() + + def OnCmdRun(self, event): + """Run command""" + try: + action = self.GetModel().GetItems()[event.pid] + if hasattr(action, "task"): + action.Update(running=True) + except IndexError: + pass + + def OnCmdPrepare(self, event): + """Prepare for running command""" + if not event.userData: + return + + event.onPrepare(item=event.userData["item"], params=event.userData["params"]) + + def OnCmdDone(self, event): + """Command done (or aborted)""" + + def time_elapsed(etime): + try: + ctime = time.time() - etime + if ctime < 60: + stime = _("%d sec") % int(ctime) + else: + mtime = int(ctime / 60) + stime = _("%(min)d min %(sec)d sec") % { + "min": mtime, + "sec": int(ctime - (mtime * 60)), + } + except KeyError: + # stopped daemon + stime = _("unknown") + + return stime + + self.goutput.GetProgressBar().SetValue(0) + self.goutput.WriteCmdLog( + "({}) {} ({})".format( + str(time.ctime()), _("Command finished"), time_elapsed(event.time) + ), + notification=event.notification, + ) + + try: + action = self.GetModel().GetItems()[event.pid] + if hasattr(action, "task"): + action.Update(running=True) + if event.pid == self._gconsole.cmdThread.GetId() - 1 and self.start_time: + self.goutput.WriteCmdLog( + "({}) {} ({})".format( + str(time.ctime()), + _("Model computation finished"), + time_elapsed(self.start_time), + ), + notification=event.notification, + ) + event = wxModelDone() + wx.PostEvent(self, event) + + except IndexError: + pass + + def OnSize(self, event): + """Window resized, save to the model""" + if not self.IsDockable(): + # model changed: window resizing is applied only if the + # window is not dockable + self.ModelChanged() + event.Skip() + + def _deleteIntermediateData(self): + """Delete intermediate data""" + rast, vect, rast3d, msg = self.model.GetIntermediateData() + if rast: + self._gconsole.RunCmd( + ["g.remove", "-f", "type=raster", "name=%s" % ",".join(rast)] + ) + if rast3d: + self._gconsole.RunCmd( + ["g.remove", "-f", "type=raster_3d", "name=%s" % ",".join(rast3d)] + ) + if vect: + self._gconsole.RunCmd( + ["g.remove", "-f", "type=vector", "name=%s" % ",".join(vect)] + ) + + self.SetStatusText( + _("%d intermediate maps deleted from current mapset") + % int(len(rast) + len(rast3d) + len(vect)) + ) + + def GetModelFile(self, ext=True): + """Get model file + + :param bool ext: False to avoid extension + """ + if not self.modelFile: + return "" + if ext: + return self.modelFile + return os.path.splitext(self.modelFile)[0] + + def OnModelDone(self, event): + """Computation finished""" + self.SetStatusText("", 0) + + # restore original files + if hasattr(self.model, "fileInput"): + for finput in self.model.fileInput: + data = self.model.fileInput[finput] + if not data: + continue + + fd = open(finput, "w") + try: + fd.write(data) + finally: + fd.close() + del self.model.fileInput + + # delete intermediate data + self._deleteIntermediateData() + + # display data if required + for data in self.model.GetData(): + if not data.HasDisplay(): + continue + + # remove existing map layers first + layers = self._giface.GetLayerList().GetLayersByName(data.GetValue()) + if layers: + for layer in layers: + self._giface.GetLayerList().DeleteLayer(layer) + + # add new map layer + self._giface.GetLayerList().AddLayer( + ltype=data.GetPrompt(), + name=data.GetValue(), + checked=True, + cmd=data.GetDisplayCmd(), + ) + + def _switchPageHandler(self, event, notification): + self._switchPage(notification=notification) + event.Skip() + + def _switchPage(self, notification): + """Manages @c 'output' notebook page according to event notification.""" + if notification == Notification.HIGHLIGHT: + self.notebook.HighlightPageByName("output") + if notification == Notification.MAKE_VISIBLE: + self.notebook.SetSelectionByName("output") + if notification == Notification.RAISE_WINDOW: + self.notebook.SetSelectionByName("output") + self.SetFocus() + self.Raise() + + def GetOptData(self, dcmd, layer, params, propwin): + """Process action data""" + if params: # add data items + data_items = [] + x = layer.GetX() + y = layer.GetY() + + for p in params["params"]: + if p.get("prompt", "") not in ( + "raster", + "vector", + "raster_3d", + "dbtable", + "stds", + "strds", + "stvds", + "str3ds", + ): + continue + + # add new data item if defined or required + if p.get("value", None) or ( + p.get("age", "old") != "old" and p.get("required", "no") == "yes" + ): + data = layer.FindData(p.get("name", "")) + if data: + data.SetValue(p.get("value", "")) + data.Update() + continue + + data = self.model.FindData(p.get("value", ""), p.get("prompt", "")) + if data: + if p.get("age", "old") == "old": + rel = ModelRelation( + parent=self, + fromShape=data, + toShape=layer, + param=p.get("name", ""), + ) + else: + rel = ModelRelation( + parent=self, + fromShape=layer, + toShape=data, + param=p.get("name", ""), + ) + layer.AddRelation(rel) + data.AddRelation(rel) + self.AddLine(rel) + data.Update() + continue + + dataClass = ( + ModelDataSeries + if p.get("prompt", "").startswith("st") + else ModelDataSingle + ) + data = dataClass( + self, + value=p.get("value", ""), + prompt=p.get("prompt", ""), + x=x, + y=y, + ) + data_items.append(data) + self._addEvent(data) + self.canvas.diagram.AddShape(data) + data.Show(False) + + if p.get("age", "old") == "old": + rel = ModelRelation( + parent=self, + fromShape=data, + toShape=layer, + param=p.get("name", ""), + ) + else: + rel = ModelRelation( + parent=self, + fromShape=layer, + toShape=data, + param=p.get("name", ""), + ) + layer.AddRelation(rel) + data.AddRelation(rel) + self.AddLine(rel) + data.Update() + + # remove dead data items + if not p.get("value", ""): + data = layer.FindData(p.get("name", "")) + if data: + remList, upList = self.model.RemoveItem(data, layer) + for item in remList: + self.canvas.diagram.RemoveShape(item) + item.__del__() + + for item in upList: + item.Update() + + # valid / parameterized ? + layer.SetValid(params) + + # arrange data items + if data_items: + dc = wx.ClientDC(self.canvas) + p = 180 / (len(data_items) - 1) if len(data_items) > 1 else 0 + rx = 200 + ry = 100 + alpha = 270 * (math.pi / 180) + for data in data_items: + data.Move(dc, x + rx * math.sin(alpha), y + ry * math.cos(alpha)) + alpha += p * (math.pi / 180) + data.Show(True) + + if dcmd: + layer.SetProperties(params, propwin) + + self.canvas.Refresh() + self.SetStatusText(layer.GetLog(), 0) + + def AddLine(self, rel): + """Add connection between model objects + + :param rel: relation + """ + fromShape = rel.GetFrom() + toShape = rel.GetTo() + + rel.SetCanvas(self) + rel.SetPen(wx.BLACK_PEN) + rel.SetBrush(wx.BLACK_BRUSH) + rel.AddArrow(ogl.ARROW_ARROW) + points = rel.GetControlPoints() + rel.MakeLineControlPoints(2) + if points: + for x, y in points: + rel.InsertLineControlPoint(point=wx.RealPoint(x, y)) + + self._addEvent(rel) + try: + fromShape.AddLine(rel, toShape) + except TypeError: + pass # bug when connecting ModelCondition and ModelLoop - to be fixed + + self.canvas.diagram.AddShape(rel) + rel.Show(True) + + def LoadModelFile(self, filename): + """Load model definition stored in GRASS Model XML file (gxm)""" + try: + self.model.LoadModel(filename) + except GException as e: + GError( + parent=self, + message=_( + "Reading model file <%s> failed.\n" + "Invalid file, unable to parse XML document.\n\n%s" + ) + % (filename, e), + showTraceback=False, + ) + return + + self.modelFile = filename + + self.RenamePage(self.baseTitle + " - " + os.path.basename(self.modelFile)) + + self.SetStatusText(_("Please wait, loading model..."), 0) + + # load actions + for item in self.model.GetItems(objType=ModelAction): + self._addEvent(item) + self.canvas.diagram.AddShape(item) + item.Show(True) + # relations/data + for rel in item.GetRelations(): + if rel.GetFrom() == item: + dataItem = rel.GetTo() + else: + dataItem = rel.GetFrom() + self._addEvent(dataItem) + self.canvas.diagram.AddShape(dataItem) + self.AddLine(rel) + dataItem.Show(True) + + # load loops + for item in self.model.GetItems(objType=ModelLoop): + self._addEvent(item) + self.canvas.diagram.AddShape(item) + item.Show(True) + + # connect items in the loop + self.DefineLoop(item) + + # load conditions + for item in self.model.GetItems(objType=ModelCondition): + self._addEvent(item) + self.canvas.diagram.AddShape(item) + item.Show(True) + + # connect items in the condition + self.DefineCondition(item) + + # load comments + for item in self.model.GetItems(objType=ModelComment): + self._addEvent(item) + self.canvas.diagram.AddShape(item) + item.Show(True) + + # load variables + self.variablePanel.Update() + self.itemPanel.Update() + self.SetStatusText("", 0) + + # final updates + for action in self.model.GetItems(objType=ModelAction): + action.SetValid(action.GetParams()) + action.Update() + + self.canvas.Refresh(True) + + def WriteModelFile(self, filename): + """Save model to model file, recover original file on error. + + :return: True on success + :return: False on failure + """ + self.ModelChanged(False) + tmpfile = tempfile.TemporaryFile(mode="w+") + try: + WriteModelFile(fd=tmpfile, model=self.model) + except Exception: + GError( + parent=self, message=_("Writing current settings to model file failed.") + ) + return False + + try: + mfile = open(filename, "w") + tmpfile.seek(0) + for line in tmpfile.readlines(): + mfile.write(line) + except OSError: + wx.MessageBox( + parent=self, + message=_("Unable to open file <%s> for writing.") % filename, + caption=_("Error"), + style=wx.OK | wx.ICON_ERROR | wx.CENTRE, + ) + return False + + mfile.close() + + return True + + def DefineLoop(self, loop): + """Define loop with given list of items""" + parent = loop + items = loop.GetItems(self.GetModel().GetItems()) + if not items: + return + + # remove defined relations first + for rel in loop.GetRelations(): + self.canvas.GetDiagram().RemoveShape(rel) + loop.Clear() + + for item in items: + rel = ModelRelation(parent=self, fromShape=parent, toShape=item) + dx = item.GetX() - parent.GetX() + dy = item.GetY() - parent.GetY() + loop.AddRelation(rel) + if dx != 0: + rel.SetControlPoints( + ( + (parent.GetX(), parent.GetY() + dy / 2), + (parent.GetX() + dx, parent.GetY() + dy / 2), + ) + ) + self.AddLine(rel) + parent = item + + # close loop + item = items[-1] + rel = ModelRelation(parent=self, fromShape=item, toShape=loop) + loop.AddRelation(rel) + self.AddLine(rel) + dx = (item.GetX() - loop.GetX()) + loop.GetWidth() / 2 + 50 + dy = item.GetHeight() / 2 + 50 + rel.MakeLineControlPoints(0) + rel.InsertLineControlPoint( + point=wx.RealPoint(loop.GetX() - loop.GetWidth() / 2, loop.GetY()) + ) + rel.InsertLineControlPoint( + point=wx.RealPoint(item.GetX(), item.GetY() + item.GetHeight() / 2) + ) + rel.InsertLineControlPoint(point=wx.RealPoint(item.GetX(), item.GetY() + dy)) + rel.InsertLineControlPoint( + point=wx.RealPoint(item.GetX() - dx, item.GetY() + dy) + ) + rel.InsertLineControlPoint(point=wx.RealPoint(item.GetX() - dx, loop.GetY())) + + self.canvas.Refresh() + + def DefineCondition(self, condition): + """Define if-else statement with given list of items""" + items = condition.GetItems(self.model.GetItems(objType=ModelAction)) + if not items["if"] and not items["else"]: + return + + parent = condition + + # remove defined relations first + for rel in condition.GetRelations(): + self.canvas.GetDiagram().RemoveShape(rel) + condition.Clear() + dxIf = condition.GetX() + condition.GetWidth() / 2 + dxElse = condition.GetX() - condition.GetWidth() / 2 + dy = condition.GetY() + for branch in items.keys(): + for item in items[branch]: + rel = ModelRelation(parent=self, fromShape=parent, toShape=item) + condition.AddRelation(rel) + self.AddLine(rel) + rel.MakeLineControlPoints(0) + if branch == "if": + rel.InsertLineControlPoint( + point=wx.RealPoint( + item.GetX() - item.GetWidth() / 2, item.GetY() + ) + ) + rel.InsertLineControlPoint(point=wx.RealPoint(dxIf, dy)) + else: + rel.InsertLineControlPoint(point=wx.RealPoint(dxElse, dy)) + rel.InsertLineControlPoint( + point=wx.RealPoint( + item.GetX() - item.GetWidth() / 2, item.GetY() + ) + ) + parent = item + + self.canvas.Refresh() + + def OnModelNew(self, event): + """Create new model""" + Debug.msg(4, "ModelerPanel.OnModelNew():") + + # ask user to save current model + if self.modelFile and self.modelChanged: + self.OnModelSave() + elif self.modelFile is None and ( + self.model.GetNumItems() > 0 or len(self.model.GetData()) > 0 + ): + dlg = wx.MessageDialog( + self, + message=_( + "Current model is not empty. " + "Do you want to store current settings " + "to model file?" + ), + caption=_("Create new model?"), + style=wx.YES_NO | wx.YES_DEFAULT | wx.CANCEL | wx.ICON_QUESTION, + ) + ret = dlg.ShowModal() + if ret == wx.ID_YES: + self.OnModelSaveAs() + elif ret == wx.ID_CANCEL: + dlg.Destroy() + return + + dlg.Destroy() + + # delete all items + self.canvas.GetDiagram().DeleteAllShapes() + self.model.Reset() + self.canvas.Refresh() + self.itemPanel.Update() + self.variablePanel.Reset() + + # no model file loaded + self.modelFile = None + self.modelChanged = False + self.RenamePage(self.baseTitle) + + def OnModelOpen(self, event): + """Load model from file""" + filename = "" + dlg = wx.FileDialog( + parent=self, + message=_("Choose model file"), + defaultDir=os.getcwd(), + wildcard=_("GRASS Model File (*.gxm)|*.gxm"), + ) + if dlg.ShowModal() == wx.ID_OK: + filename = dlg.GetPath() + + if not filename: + return + + Debug.msg(4, "ModelerPanel.OnModelOpen(): filename=%s" % filename) + + # close current model + self.OnModelClose() + + self.LoadModelFile(filename) + + self.modelFile = filename + self.RenamePage(self.baseTitle + " - " + os.path.basename(self.modelFile)) + self.SetStatusText( + _("%(items)d items (%(actions)d actions) loaded into model") + % { + "items": self.model.GetNumItems(), + "actions": self.model.GetNumItems(actionOnly=True), + }, + 0, + ) + + def OnModelSave(self, event=None): + """Save model to file""" + if self.modelFile and self.modelChanged: + dlg = wx.MessageDialog( + self, + message=_( + "Model file <%s> already exists. " + "Do you want to overwrite this file?" + ) + % self.modelFile, + caption=_("Save model"), + style=wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION, + ) + if dlg.ShowModal() == wx.ID_NO: + dlg.Destroy() + else: + Debug.msg(4, "ModelerPanel.OnModelSave(): filename=%s" % self.modelFile) + self.WriteModelFile(self.modelFile) + self.SetStatusText(_("File <%s> saved") % self.modelFile, 0) + self.RenamePage( + self.baseTitle + " - " + os.path.basename(self.modelFile) + ) + elif not self.modelFile: + self.OnModelSaveAs() + + def OnModelSaveAs(self, event=None): + """Create model to file as""" + filename = "" + dlg = wx.FileDialog( + parent=self, + message=_("Choose file to save current model"), + defaultDir=os.getcwd(), + wildcard=_("GRASS Model File (*.gxm)|*.gxm"), + style=wx.FD_SAVE, + ) + + if dlg.ShowModal() == wx.ID_OK: + filename = dlg.GetPath() + + if not filename: + return + + # check for extension + if filename[-4:] != ".gxm": + filename += ".gxm" + + if os.path.exists(filename): + dlg = wx.MessageDialog( + parent=self, + message=_( + "Model file <%s> already exists. " + "Do you want to overwrite this file?" + ) + % filename, + caption=_("File already exists"), + style=wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION, + ) + if dlg.ShowModal() != wx.ID_YES: + dlg.Destroy() + return + + Debug.msg(4, "GMFrame.OnModelSaveAs(): filename=%s" % filename) + + self.WriteModelFile(filename) + self.modelFile = filename + self.RenamePage(self.baseTitle + " - " + os.path.basename(self.modelFile)) + self.SetStatusText(_("File <%s> saved") % self.modelFile, 0) + + def OnModelClose(self, event=None): + """Close model file""" + Debug.msg(4, "ModelerPanel.OnModelClose(): file=%s" % self.modelFile) + # ask user to save current model + if self.modelFile and self.modelChanged: + self.OnModelSave() + elif self.modelFile is None and ( + self.model.GetNumItems() > 0 or len(self.model.GetData()) > 0 + ): + dlg = wx.MessageDialog( + self, + message=_( + "Current model is not empty. " + "Do you want to store current settings " + "to model file?" + ), + caption=_("Create new model?"), + style=wx.YES_NO | wx.YES_DEFAULT | wx.CANCEL | wx.ICON_QUESTION, + ) + ret = dlg.ShowModal() + if ret == wx.ID_YES: + self.OnModelSaveAs() + elif ret == wx.ID_CANCEL: + dlg.Destroy() + return + + dlg.Destroy() + + self.modelFile = None + self.RenamePage(self.baseTitle) + + self.canvas.GetDiagram().DeleteAllShapes() + self.model.Reset() + + self.canvas.Refresh() + + def OnRunModel(self, event): + """Run entire model""" + self.start_time = time.time() + self.model.Run(self._gconsole, self.OnModelDone, parent=self) + + def OnExportImage(self, event): + """Export model to image (default image)""" + xminImg = 0 + xmaxImg = 0 + yminImg = 0 + ymaxImg = 0 + # get current size of canvas + for shape in self.canvas.GetDiagram().GetShapeList(): + w, h = shape.GetBoundingBoxMax() + x = shape.GetX() + y = shape.GetY() + xmin = x - w / 2 + xmax = x + w / 2 + ymin = y - h / 2 + ymax = y + h / 2 + if xmin < xminImg: + xminImg = xmin + if xmax > xmaxImg: + xmaxImg = xmax + if ymin < yminImg: + yminImg = ymin + if ymax > ymaxImg: + ymaxImg = ymax + size = wx.Size(int(xmaxImg - xminImg) + 50, int(ymaxImg - yminImg) + 50) + bitmap = EmptyBitmap(width=size.width, height=size.height) + + filetype, ltype = GetImageHandlers(ImageFromBitmap(bitmap)) + + dlg = wx.FileDialog( + parent=self, + message=_( + "Choose a file name to save the image (no need to add extension)" + ), + defaultDir="", + defaultFile="", + wildcard=filetype, + style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT, + ) + + if dlg.ShowModal() == wx.ID_OK: + path = dlg.GetPath() + if not path: + dlg.Destroy() + return + + base, ext = os.path.splitext(path) + fileType = ltype[dlg.GetFilterIndex()]["type"] + extType = ltype[dlg.GetFilterIndex()]["ext"] + if ext != extType: + path = base + "." + extType + + dc = wx.MemoryDC(bitmap) + dc.SetBackground(wx.WHITE_BRUSH) + dc.SetBackgroundMode(wx.SOLID) + + self.canvas.GetDiagram().Clear(dc) + self.canvas.GetDiagram().Redraw(dc) + + bitmap.SaveFile(path, fileType) + self.SetStatusText(_("Model exported to <%s>") % path) + + dlg.Destroy() + + def OnExportPython(self, event=None, text=None): + """Export model to Python script""" + filename = self.pythonPanel.SaveAs(force=True) + self.SetStatusText(_("Model exported to <%s>") % filename) + + def OnPreferences(self, event): + """Open preferences dialog""" + dlg = PreferencesDialog(parent=self, giface=self._giface) + dlg.CenterOnParent() + + dlg.Show() + self.canvas.Refresh() + + def OnAddAction(self, event): + """Add action to model""" + if self.searchDialog is None: + self.searchDialog = ModelSearchDialog(parent=self, giface=self._giface) + self.searchDialog.CentreOnParent() + else: + self.searchDialog.Reset() + + if self.searchDialog.ShowModal() == wx.ID_CANCEL: + self.searchDialog.Hide() + return + + cmd = self.searchDialog.GetCmd() + self.searchDialog.Hide() + + self.ModelChanged() + + # add action to canvas + x, y = self.canvas.GetNewShapePos() + label, comment = self.searchDialog.GetLabel() + action = ModelAction( + self.model, + cmd=cmd, + x=x, + y=y, + id=self.model.GetNextId(), + label=label, + comment=comment, + ) + overwrite = self.model.GetProperties().get("overwrite", None) + if overwrite is not None: + action.GetTask().set_flag("overwrite", overwrite) + + self.canvas.diagram.AddShape(action) + action.Show(True) + + self._addEvent(action) + self.model.AddItem(action) + + self.itemPanel.Update() + self.canvas.Refresh() + time.sleep(0.1) + + # show properties dialog + win = action.GetPropDialog() + if not win: + gmodule = GUI( + parent=self, + show=True, + giface=GraphicalModelerGrassInterface(self.model), + ) + gmodule.ParseCommand( + action.GetLog(string=False), + completed=(self.GetOptData, action, action.GetParams()), + ) + elif win and not win.IsShown(): + win.Show() + + if win: + win.Raise() + + def OnAddData(self, event): + """Add data item to model""" + # add action to canvas + width, height = self.canvas.GetSize() + data = ModelData( + self, x=width / 2 + self._randomShift(), y=height / 2 + self._randomShift() + ) + + dlg = ModelDataDialog(parent=self, shape=data) + data.SetPropDialog(dlg) + dlg.CentreOnParent() + ret = dlg.ShowModal() + dlg.Destroy() + if ret != wx.ID_OK: + return + + data.Update() + self.canvas.diagram.AddShape(data) + data.Show(True) + + self.ModelChanged() + + self._addEvent(data) + self.model.AddItem(data) + + self.canvas.Refresh() + + def OnAddComment(self, event): + """Add comment to the model""" + dlg = CustomTextEntryDialog( + parent=self, + message=_("Comment:"), + caption=_("Add comment"), + textStyle=wx.TE_MULTILINE, + textSize=(300, 75), + ) + + if dlg.ShowModal() == wx.ID_OK: + comment = dlg.GetValue() + if not comment: + GError(_("Empty comment. Nothing to add to the model."), parent=self) + else: + x, y = self.canvas.GetNewShapePos() + commentObj = ModelComment( + self.model, + x=x, + y=y, + id=self.model.GetNextId(), + label=comment, + ) + self.canvas.diagram.AddShape(commentObj) + commentObj.Show(True) + self._addEvent(commentObj) + self.model.AddItem(commentObj) + + self.canvas.Refresh() + self.ModelChanged() + + dlg.Destroy() + + def OnDefineRelation(self, event): + """Define relation between data and action items""" + self.canvas.SetCursor(self.cursors["cross"]) + self.defineRelation = {"from": None, "to": None} + + def OnDefineLoop(self, event): + """Define new loop in the model + + .. todo:: + move to ModelCanvas? + """ + self.ModelChanged() + + width, height = self.canvas.GetSize() + loop = ModelLoop( + self, x=width / 2, y=height / 2, id=self.model.GetNumItems() + 1 + ) + self.canvas.diagram.AddShape(loop) + loop.Show(True) + + self._addEvent(loop) + self.model.AddItem(loop) + + self.canvas.Refresh() + + def OnDefineCondition(self, event): + """Define new condition in the model + + .. todo:: + move to ModelCanvas? + """ + self.ModelChanged() + + width, height = self.canvas.GetSize() + cond = ModelCondition( + self, x=width / 2, y=height / 2, id=self.model.GetNumItems() + 1 + ) + self.canvas.diagram.AddShape(cond) + cond.Show(True) + + self._addEvent(cond) + self.model.AddItem(cond) + + self.canvas.Refresh() + + def OnRemoveItem(self, event): + """Remove shape""" + self.GetCanvas().RemoveSelected() + + def OnModelProperties(self, event): + """Model properties dialog""" + dlg = PropertiesDialog(parent=self) + dlg.CentreOnParent() + properties = self.model.GetProperties() + dlg.Init(properties) + if dlg.ShowModal() == wx.ID_OK: + self.ModelChanged() + for key, value in six.iteritems(dlg.GetValues()): + properties[key] = value + for action in self.model.GetItems(objType=ModelAction): + action.GetTask().set_flag("overwrite", properties["overwrite"]) + + dlg.Destroy() + + def OnDeleteData(self, event): + """Delete intermediate data""" + rast, vect, rast3d, msg = self.model.GetIntermediateData() + + if not rast and not vect and not rast3d: + GMessage(parent=self, message=_("No intermediate data to delete.")) + return + + dlg = wx.MessageDialog( + parent=self, + message=_("Do you want to permanently delete data?%s" % msg), + caption=_("Delete intermediate data?"), + style=wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION, + ) + + ret = dlg.ShowModal() + dlg.Destroy() + if ret == wx.ID_YES: + self._deleteIntermediateData() + + def OnValidateModel(self, event, showMsg=True): + """Validate entire model""" + if self.model.GetNumItems() < 1: + GMessage(parent=self, message=_("Model is empty. Nothing to validate.")) + return + + self.SetStatusText(_("Validating model..."), 0) + errList = self.model.Validate() + self.SetStatusText("", 0) + + if errList: + GWarning( + parent=self, message=_("Model is not valid.\n\n%s") % "\n".join(errList) + ) + else: + GMessage(parent=self, message=_("Model is valid.")) + + def OnHelp(self, event): + """Show help""" + self._giface.Help(entry="wxGUI.gmodeler") + + def OnAbout(self, event): + """Display About window""" + ShowAboutDialog(prgName=_("wxGUI Graphical Modeler"), startYear="2010") + + def OnCanvasRefresh(self, event): + """Refresh canvas""" + self.SetStatusText(_("Redrawing model..."), 0) + self.GetCanvas().Refresh() + self.SetStatusText("", 0) + + def OnVariables(self, event): + """Switch to variables page""" + self.notebook.SetSelectionByName("variables") + + def OnCloseWindow(self, event): + """Close window""" + if self.modelChanged and UserSettings.Get( + group="manager", key="askOnQuit", subkey="enabled" + ): + if self.modelFile: + message = _("Do you want to save changes in the model?") + else: + message = _( + "Do you want to store current model settings to model file?" + ) + + # ask user to save current settings + dlg = wx.MessageDialog( + self, + message=message, + caption=_("Quit Graphical Modeler"), + style=wx.YES_NO + | wx.YES_DEFAULT + | wx.CANCEL + | wx.ICON_QUESTION + | wx.CENTRE, + ) + ret = dlg.ShowModal() + if ret == wx.ID_YES: + if not self.modelFile: + self.OnModelSaveAs() + else: + self.WriteModelFile(self.modelFile) + elif ret == wx.ID_CANCEL: + dlg.Destroy() + return + dlg.Destroy() + + self._onCloseWindow(event) + + +class VariablePanel(wx.Panel): + def __init__(self, parent, id=wx.ID_ANY, **kwargs): + """Manage model variables panel""" + self.parent = parent + + wx.Panel.__init__(self, parent=parent, id=id, **kwargs) + + self.listBox = StaticBox( + parent=self, + id=wx.ID_ANY, + label=" %s " % _("List of variables - right-click to delete"), + ) + + self.list = VariableListCtrl( + parent=self, + columns=[_("Name"), _("Data type"), _("Default value"), _("Description")], + frame=self.parent, + ) + + # add new category + self.addBox = StaticBox( + parent=self, id=wx.ID_ANY, label=" %s " % _("Add new variable") + ) + self.name = TextCtrl(parent=self, id=wx.ID_ANY) + wx.CallAfter(self.name.SetFocus) + self.type = wx.Choice( + parent=self, + id=wx.ID_ANY, + choices=[ + _("integer"), + _("float"), + _("string"), + _("raster"), + _("vector"), + _("region"), + _("mapset"), + _("file"), + _("dir"), + ], + ) + self.type.SetSelection(2) # string + self.value = TextCtrl(parent=self, id=wx.ID_ANY) + self.desc = TextCtrl(parent=self, id=wx.ID_ANY) + + # buttons + self.btnAdd = Button(parent=self, id=wx.ID_ADD) + self.btnAdd.SetToolTip(_("Add new variable to the model")) + self.btnAdd.Enable(False) + + # bindings + self.name.Bind(wx.EVT_TEXT, self.OnText) + self.value.Bind(wx.EVT_TEXT, self.OnText) + self.desc.Bind(wx.EVT_TEXT, self.OnText) + self.btnAdd.Bind(wx.EVT_BUTTON, self.OnAdd) + + self._layout() + + def _layout(self): + """Layout dialog""" + listSizer = wx.StaticBoxSizer(self.listBox, wx.VERTICAL) + listSizer.Add(self.list, proportion=1, flag=wx.EXPAND) + + addSizer = wx.StaticBoxSizer(self.addBox, wx.VERTICAL) + gridSizer = wx.GridBagSizer(hgap=5, vgap=5) + gridSizer.Add( + StaticText(parent=self, id=wx.ID_ANY, label="%s:" % _("Name")), + flag=wx.ALIGN_CENTER_VERTICAL, + pos=(0, 0), + ) + gridSizer.Add(self.name, pos=(0, 1), flag=wx.EXPAND) + gridSizer.Add( + StaticText(parent=self, id=wx.ID_ANY, label="%s:" % _("Data type")), + flag=wx.ALIGN_CENTER_VERTICAL, + pos=(0, 2), + ) + gridSizer.Add(self.type, pos=(0, 3)) + gridSizer.Add( + StaticText(parent=self, id=wx.ID_ANY, label="%s:" % _("Default value")), + flag=wx.ALIGN_CENTER_VERTICAL, + pos=(1, 0), + ) + gridSizer.Add(self.value, pos=(1, 1), span=(1, 3), flag=wx.EXPAND) + gridSizer.Add( + StaticText(parent=self, id=wx.ID_ANY, label="%s:" % _("Description")), + flag=wx.ALIGN_CENTER_VERTICAL, + pos=(2, 0), + ) + gridSizer.Add(self.desc, pos=(2, 1), span=(1, 3), flag=wx.EXPAND) + gridSizer.AddGrowableCol(1) + addSizer.Add(gridSizer, flag=wx.EXPAND) + addSizer.Add(self.btnAdd, proportion=0, flag=wx.TOP | wx.ALIGN_RIGHT, border=5) + + mainSizer = wx.BoxSizer(wx.VERTICAL) + mainSizer.Add(listSizer, proportion=1, flag=wx.EXPAND | wx.ALL, border=5) + mainSizer.Add( + addSizer, + proportion=0, + flag=wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, + border=5, + ) + + self.SetSizer(mainSizer) + mainSizer.Fit(self) + + def OnText(self, event): + """Text entered""" + if self.name.GetValue(): + self.btnAdd.Enable() + else: + self.btnAdd.Enable(False) + + def OnAdd(self, event): + """Add new variable to the list""" + msg = self.list.Append( + self.name.GetValue(), + self.type.GetStringSelection(), + self.value.GetValue(), + self.desc.GetValue(), + ) + self.name.SetValue("") + self.name.SetFocus() + + if msg: + GError(parent=self, message=msg) + else: + self.type.SetSelection(2) # string + self.value.SetValue("") + self.desc.SetValue("") + self.UpdateModelVariables() + + def UpdateModelVariables(self): + """Update model variables""" + variables = dict() + for values in six.itervalues(self.list.GetData()): + name = values[0] + variables[name] = {"type": str(values[1])} + if values[2]: + variables[name]["value"] = values[2] + if values[3]: + variables[name]["description"] = values[3] + + self.parent.GetModel().SetVariables(variables) + self.parent.ModelChanged() + + def Update(self): + """Reload list of variables""" + self.list.OnReload(None) + + def Reset(self): + """Remove all variables""" + self.list.DeleteAllItems() + self.parent.GetModel().SetVariables({}) + + +class ItemPanel(wx.Panel): + def __init__(self, parent, id=wx.ID_ANY, **kwargs): + """Manage model items""" + self.parent = parent + + wx.Panel.__init__(self, parent=parent, id=id, **kwargs) + + self.listBox = StaticBox( + parent=self, + id=wx.ID_ANY, + label=" %s " % _("List of items - right-click to delete"), + ) + + self.list = ItemListCtrl( + parent=self, + columns=[_("Label"), _("In loop"), _("Parameterized"), _("Command")], + columnsNotEditable=[1, 2, 3], + frame=self.parent, + ) + + self.btnMoveUp = Button(parent=self, id=wx.ID_UP) + self.btnMoveDown = Button(parent=self, id=wx.ID_DOWN) + self.btnRefresh = Button(parent=self, id=wx.ID_REFRESH) + + self.btnMoveUp.Bind(wx.EVT_BUTTON, self.OnMoveItemsUp) + self.btnMoveDown.Bind(wx.EVT_BUTTON, self.OnMoveItemsDown) + self.btnRefresh.Bind(wx.EVT_BUTTON, self.list.OnReload) + + self._layout() + + def _layout(self): + """Layout dialog""" + listSizer = wx.StaticBoxSizer(self.listBox, wx.VERTICAL) + listSizer.Add(self.list, proportion=1, flag=wx.EXPAND) + + manageSizer = wx.BoxSizer(wx.VERTICAL) + manageSizer.Add(self.btnMoveUp, border=5, flag=wx.ALL) + manageSizer.Add(self.btnMoveDown, border=5, flag=wx.LEFT | wx.RIGHT | wx.BOTTOM) + manageSizer.Add(self.btnRefresh, border=5, flag=wx.LEFT | wx.RIGHT) + + mainSizer = wx.BoxSizer(wx.HORIZONTAL) + mainSizer.Add(listSizer, proportion=1, flag=wx.EXPAND | wx.ALL, border=3) + mainSizer.Add(manageSizer, proportion=0, flag=wx.EXPAND | wx.ALL, border=3) + + self.SetSizer(mainSizer) + mainSizer.Fit(self) + + def Update(self): + """Reload list of variables""" + self.list.OnReload(None) + + def _getSelectedItems(self): + """Get list of selected items, indices start at 0""" + items = [] + current = -1 + while True: + next = self.list.GetNextSelected(current) + if next == -1: + break + items.append(next) + current = next + + if not items: + GMessage(_("No items to selected."), parent=self) + + return items + + def OnMoveItemsUp(self, event): + """Item moved up, update action ids""" + items = self._getSelectedItems() + if not items: + return + self.list.MoveItems(items, up=True) + self.parent.GetCanvas().Refresh() + self.parent.ModelChanged() + + def OnMoveItemsDown(self, event): + """Item moved up, update action ids""" + items = self._getSelectedItems() + if not items: + return + self.list.MoveItems(items, up=False) + self.parent.GetCanvas().Refresh() + self.parent.ModelChanged() + + +class PythonPanel(wx.Panel): + """Model as a Python script of choice.""" + + def __init__(self, parent, id=wx.ID_ANY, **kwargs): + """Initialize the panel.""" + self.parent = parent + + wx.Panel.__init__(self, parent=parent, id=id, **kwargs) + + # variable for a temp file to run Python scripts + self.filename = None + # default values of variables that will be changed if the desired + # script type is changed + self.write_object = WritePythonFile + + self.bodyBox = StaticBox( + parent=self, id=wx.ID_ANY, label=" %s " % _("Python script") + ) + self.body = PyStc(parent=self, statusbar=self.parent.GetStatusBar()) + if IsDark(): + SetDarkMode(self.body) + + self.btnRun = Button(parent=self, id=wx.ID_ANY, label=_("&Run")) + self.btnRun.SetToolTip(_("Run script")) + self.Bind(wx.EVT_BUTTON, self.OnRun, self.btnRun) + self.btnSaveAs = Button(parent=self, id=wx.ID_SAVEAS) + self.btnSaveAs.SetToolTip(_("Save the script to a file")) + self.Bind(wx.EVT_BUTTON, self.OnSaveAs, self.btnSaveAs) + self.btnRefresh = Button(parent=self, id=wx.ID_REFRESH) + self.btnRefresh.SetToolTip( + _( + "Refresh the script based on the model.\n" + "It will discard all local changes." + ) + ) + self.script_type_box = wx.Choice( + parent=self, + id=wx.ID_ANY, + choices=[ + _("Python"), + _("PyWPS"), + ], + ) + self.script_type_box.SetSelection(0) # Python + self.Bind(wx.EVT_BUTTON, self.OnRefresh, self.btnRefresh) + self.Bind( + wx.EVT_CHOICE, + self.OnChangeScriptType, + self.script_type_box, + ) + + self._layout() + + def _layout(self): + sizer = wx.BoxSizer(wx.VERTICAL) + bodySizer = wx.StaticBoxSizer(self.bodyBox, wx.HORIZONTAL) + btnSizer = wx.BoxSizer(wx.HORIZONTAL) + + bodySizer.Add(self.body, proportion=1, flag=wx.EXPAND | wx.ALL, border=3) + + btnSizer.Add( + StaticText( + parent=self, id=wx.ID_ANY, label="%s:" % _("Python script type") + ), + flag=wx.ALIGN_CENTER_VERTICAL, + ) + btnSizer.Add(self.script_type_box, proportion=0, flag=wx.RIGHT, border=5) + btnSizer.AddStretchSpacer() + btnSizer.Add(self.btnRefresh, proportion=0, flag=wx.LEFT | wx.RIGHT, border=5) + btnSizer.Add(self.btnSaveAs, proportion=0, flag=wx.RIGHT, border=5) + btnSizer.Add(self.btnRun, proportion=0, flag=wx.RIGHT, border=5) + + sizer.Add(bodySizer, proportion=1, flag=wx.EXPAND | wx.ALL, border=3) + sizer.Add(btnSizer, proportion=0, flag=wx.EXPAND | wx.ALL, border=3) + + sizer.Fit(self) + sizer.SetSizeHints(self) + self.SetSizer(sizer) + + def RefreshScript(self): + """Refresh the script. + + :return: True on refresh + :return: False script hasn't been updated + """ + if len(self.parent.GetModel().GetItems()) == 0: + # no need to fully parse an empty script + self.body.SetText("") + return True + + if self.body.modified: + dlg = wx.MessageDialog( + self, + message=_( + "{} script is locally modified. " + "Refresh will discard all changes. " + "Do you really want to continue?".format(self.body.script_type) + ), + caption=_("Update"), + style=wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION | wx.CENTRE, + ) + ret = dlg.ShowModal() + dlg.Destroy() + if ret == wx.ID_NO: + return False + + fd = tempfile.TemporaryFile(mode="r+") + grassAPI = UserSettings.Get(group="modeler", key="grassAPI", subkey="selection") + self.write_object( + fd, + self.parent.GetModel(), + grassAPI="script" if grassAPI == 0 else "pygrass", + ) + + fd.seek(0) + self.body.SetText(fd.read()) + fd.close() + + self.body.modified = False + + return True + + def SaveAs(self, force=False): + """Save the script to a file. + + :return: filename + """ + filename = "" + dlg = wx.FileDialog( + parent=self, + message=_("Choose file to save"), + defaultFile=os.path.basename(self.parent.GetModelFile(ext=False)), + defaultDir=os.getcwd(), + wildcard=_("Python script (*.py)|*.py"), + style=wx.FD_SAVE, + ) + + if dlg.ShowModal() == wx.ID_OK: + filename = dlg.GetPath() + + if not filename: + return "" + + # check for extension + if filename[-3:] != ".py": + filename += ".py" + + if os.path.exists(filename): + dlg = wx.MessageDialog( + self, + message=_( + "File <%s> already exists. " "Do you want to overwrite this file?" + ) + % filename, + caption=_("Save file"), + style=wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION, + ) + if dlg.ShowModal() == wx.ID_NO: + dlg.Destroy() + return "" + + dlg.Destroy() + + fd = open(filename, "w") + try: + if force: + self.write_object(fd, self.parent.GetModel()) + else: + fd.write(self.body.GetText()) + finally: + fd.close() + + # executable file + os.chmod(filename, stat.S_IRWXU | stat.S_IWUSR) + + return filename + + def OnRun(self, event): + """Run Python script""" + self.filename = grass.tempfile() + try: + fd = open(self.filename, "w") + fd.write(self.body.GetText()) + except OSError as e: + GError(_("Unable to launch Python script. %s") % e, parent=self) + return + finally: + fd.close() + mode = stat.S_IMODE(os.lstat(self.filename)[stat.ST_MODE]) + os.chmod(self.filename, mode | stat.S_IXUSR) + + for item in self.parent.GetModel().GetItems(): + if ( + len(item.GetParameterizedParams()["params"]) + + len(item.GetParameterizedParams()["flags"]) + > 0 + ): + self.parent._gconsole.RunCmd( + [fd.name, "--ui"], skipInterface=False, onDone=self.OnDone + ) + break + else: + self.parent._gconsole.RunCmd( + [fd.name], skipInterface=True, onDone=self.OnDone + ) + + event.Skip() + + def OnDone(self, event): + """Python script finished""" + try_remove(self.filename) + self.filename = None + + def OnChangeScriptType(self, event): + new_script_type = self.script_type_box.GetStringSelection() + if new_script_type == "Python": + self.write_object = WritePythonFile + elif new_script_type == "PyWPS": + self.write_object = WritePyWPSFile + + if self.RefreshScript(): + self.body.script_type = new_script_type + self.parent.SetStatusText( + _("{} script is up-to-date".format(self.body.script_type)), + 0, + ) + + self.script_type_box.SetStringSelection(self.body.script_type) + + if self.body.script_type == "Python": + self.write_object = WritePythonFile + self.btnRun.Enable() + self.btnRun.SetToolTip(_("Run script")) + elif self.body.script_type == "PyWPS": + self.write_object = WritePyWPSFile + self.btnRun.Disable() + self.btnRun.SetToolTip( + _("Run script - enabled only for basic Python scripts") + ) + + def OnRefresh(self, event): + """Refresh the script.""" + if self.RefreshScript(): + self.parent.SetStatusText( + _("{} script is up-to-date".format(self.body.script_type)), + 0, + ) + event.Skip() + + def OnSaveAs(self, event): + """Save the script to a file.""" + self.SaveAs(force=False) + event.Skip() + + def IsModified(self): + """Check if the script has been modified.""" + return self.body.modified + + def IsEmpty(self): + """Check if the script is empty.""" + return len(self.body.GetText()) == 0 diff --git a/gui/wxpython/gmodeler/toolbars.py b/gui/wxpython/gmodeler/toolbars.py index cee01d094db..ca6a75b4804 100644 --- a/gui/wxpython/gmodeler/toolbars.py +++ b/gui/wxpython/gmodeler/toolbars.py @@ -6,7 +6,7 @@ Classes: - toolbars::ModelerToolbar -(C) 2010-2011 by the GRASS Development Team +(C) 2010-2023 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. @@ -16,6 +16,8 @@ import sys +import wx + from gui_core.toolbars import BaseToolbar, BaseIcons from icons.icon import MetaIcon @@ -39,29 +41,44 @@ def __init__(self, parent): def _toolbarData(self): """Toolbar data""" icons = { - "new": MetaIcon(img="create", label=_("Create new model (Ctrl+N)")), - "open": MetaIcon(img="open", label=_("Load model from file (Ctrl+O)")), + "new": MetaIcon( + img="create", + label=_("Create new model") + " (Ctrl+Alt+N)", + ), + "open": MetaIcon( + img="open", + label=_("Load model from file") + " (Ctrl+Alt+O)", + ), "save": MetaIcon( - img="save", label=_("Save current model to file (Ctrl+S)") + img="save", + label=_("Save current model to file") + " (Ctrl+Alt+S)", ), "toImage": MetaIcon(img="image-export", label=_("Export model to image")), "toPython": MetaIcon( - img="python-export", label=_("Export model to Python script") + img="python-export", + label=_("Export model to Python script") + " (Ctrl+Alt+P)", ), "actionAdd": MetaIcon( - img="module-add", label=_("Add GRASS tool (module) to model") + img="module-add", + label=_("Add GRASS tool (module) to model") + " (Ctrl+Alt+A)", + ), + "dataAdd": MetaIcon( + img="data-add", label=_("Add data to model") + " (Ctrl+Alt+D)" ), - "dataAdd": MetaIcon(img="data-add", label=_("Add data to model")), "relation": MetaIcon( img="relation-create", label=_("Manually define relation between data and commands"), ), - "loop": MetaIcon(img="loop-add", label=_("Add loop/series to model")), - "comment": MetaIcon(img="label-add", label=_("Add comment to model")), - "run": MetaIcon(img="execute", label=_("Run model")), + "loop": MetaIcon( + img="loop-add", label=_("Add loop/series to model") + " (Ctrl+Alt+L)" + ), + "comment": MetaIcon( + img="label-add", label=_("Add comment to model") + " (Ctrl+Alt+#)" + ), + "run": MetaIcon(img="execute", label=_("Run model") + " (Ctrl+Alt+R)"), "validate": MetaIcon(img="check", label=_("Validate model")), - "settings": BaseIcons["settings"], - "properties": MetaIcon(img="options", label=_("Show model properties")), + "settings": MetaIcon(img="modeler-settings", label=_("Modeler settings")), + "properties": MetaIcon(img="options", label=_("Set model properties")), "variables": MetaIcon( img="modeler-variables", label=_("Manage model variables") ), @@ -70,96 +87,113 @@ def _toolbarData(self): "quit": BaseIcons["quit"], } - return self._getToolbarData( + data = ( ( + ("new", icons["new"].label.rsplit(" ", 1)[0]), + icons["new"], + self.parent.OnModelNew, + ), + ( + ("open", icons["open"].label.rsplit(" ", 1)[0]), + icons["open"], + self.parent.OnModelOpen, + ), + ( + ("save", icons["save"].label.rsplit(" ", 1)[0]), + icons["save"], + self.parent.OnModelSave, + ), + ( + ("image", icons["toImage"].label.rsplit(" ", 1)[0]), + icons["toImage"], + self.parent.OnExportImage, + ), + ( + ("python", icons["toPython"].label), + icons["toPython"], + self.parent.OnExportPython, + ), + (None,), + ( + ("action", icons["actionAdd"].label), + icons["actionAdd"], + self.parent.OnAddAction, + ), + ( + ("data", icons["dataAdd"].label), + icons["dataAdd"], + self.parent.OnAddData, + ), + ( + ("relation", icons["relation"].label), + icons["relation"], + self.parent.OnDefineRelation, + ), + ( + ("loop", icons["loop"].label), + icons["loop"], + self.parent.OnDefineLoop, + ), + ( + ("comment", icons["comment"].label), + icons["comment"], + self.parent.OnAddComment, + ), + (None,), + ( + ("redraw", icons["redraw"].label), + icons["redraw"], + self.parent.OnCanvasRefresh, + ), + ( + ("validate", icons["validate"].label), + icons["validate"], + self.parent.OnValidateModel, + ), + ( + ("run", icons["run"].label), + icons["run"], + self.parent.OnRunModel, + ), + (None,), + ( + ("variables", icons["variables"].label), + icons["variables"], + self.parent.OnVariables, + ), + ( + ("properties", icons["properties"].label), + icons["properties"], + self.parent.OnModelProperties, + ), + (None,), + ( + ("settings", icons["settings"].label), + icons["settings"], + self.parent.OnPreferences, + ), + ( + ("help", icons["help"].label), + icons["help"], + self.parent.OnHelp, + ), + (None,), + ) + if self.parent.IsDockable(): + data += ( ( - ("new", icons["new"].label.rsplit(" ", 1)[0]), - icons["new"], - self.parent.OnModelNew, - ), - ( - ("open", icons["open"].label.rsplit(" ", 1)[0]), - icons["open"], - self.parent.OnModelOpen, - ), - ( - ("save", icons["save"].label.rsplit(" ", 1)[0]), - icons["save"], - self.parent.OnModelSave, - ), - ( - ("image", icons["toImage"].label.rsplit(" ", 1)[0]), - icons["toImage"], - self.parent.OnExportImage, - ), - ( - ("python", icons["toPython"].label), - icons["toPython"], - self.parent.OnExportPython, - ), - (None,), - ( - ("action", icons["actionAdd"].label), - icons["actionAdd"], - self.parent.OnAddAction, - ), - ( - ("data", icons["dataAdd"].label), - icons["dataAdd"], - self.parent.OnAddData, - ), - ( - ("relation", icons["relation"].label), - icons["relation"], - self.parent.OnDefineRelation, - ), - ( - ("loop", icons["loop"].label), - icons["loop"], - self.parent.OnDefineLoop, - ), - ( - ("comment", icons["comment"].label), - icons["comment"], - self.parent.OnAddComment, - ), - (None,), - ( - ("redraw", icons["redraw"].label), - icons["redraw"], - self.parent.OnCanvasRefresh, - ), - ( - ("validate", icons["validate"].label), - icons["validate"], - self.parent.OnValidateModel, - ), - ( - ("run", icons["run"].label), - icons["run"], - self.parent.OnRunModel, - ), - (None,), - ( - ("variables", icons["variables"].label), - icons["variables"], - self.parent.OnVariables, - ), - ( - ("settings", icons["settings"].label), - icons["settings"], - self.parent.OnPreferences, - ), - ( - ("help", icons["help"].label), - icons["help"], - self.parent.OnHelp, - ), - (None,), - ( - ("quit", icons["quit"].label), - icons["quit"], - self.parent.OnCloseWindow, + ("docking", BaseIcons["docking"].label), + BaseIcons["docking"], + self.parent.OnDockUndock, + wx.ITEM_CHECK, ), ) + data += ( + ( + ("quit", icons["quit"].label), + icons["quit"], + self.parent.OnCloseWindow, + ), ) + + return self._getToolbarData(data) diff --git a/gui/wxpython/gui_core/menu.py b/gui/wxpython/gui_core/menu.py index d616b73235c..55ba0d40d48 100644 --- a/gui/wxpython/gui_core/menu.py +++ b/gui/wxpython/gui_core/menu.py @@ -4,11 +4,13 @@ @brief Menu classes for wxGUI Classes: + - menu::MenuBase - menu::Menu + - menu::MenuItem - menu::SearchModuleWindow - menu::RecentFilesMenu -(C) 2010-2013 by the GRASS Development Team +(C) 2010-2024 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. @@ -35,17 +37,21 @@ from grass.pydispatch.signal import Signal -class Menu(wx.MenuBar): - def __init__(self, parent, model): - """Creates menubar""" - wx.MenuBar.__init__(self) +class MenuBase: + def __init__(self, parent, model, class_handler=None): + """Base menu class. + + Base class for Menu and MenuItem classes. + + :param parent: parent object + :param model: model menu data object + :param class_handler: handler object if None parent is used + """ self.parent = parent self.model = model self.menucmd = dict() self.bmpsize = (16, 16) - - for child in self.model.root.children: - self.Append(self._createMenu(child), child.label) + self.class_handler = class_handler if class_handler is not None else parent def _createMenu(self, node): """Creates menu""" @@ -115,7 +121,7 @@ def _createMenuItem( ): menuItem.Enable(False) - rhandler = eval("self.parent." + handler) + rhandler = eval("self.class_handler." + handler) # nosec B307 self.parent.Bind(wx.EVT_MENU, rhandler, menuItem) def GetData(self): @@ -143,6 +149,39 @@ def OnMenuHighlight(self, event): event.Skip() +class Menu(MenuBase, wx.MenuBar): + def __init__(self, parent, model, class_handler=None): + """Menu Bar class. + + :param parent: parent object + :param model: model menu data object + :param class_handler: handler object if None parent is used + """ + MenuBase.__init__(self, parent, model, class_handler) + wx.MenuBar.__init__(self) + + for child in self.model.root.children: + self.Append(self._createMenu(child), child.label) + + +class MenuItem(MenuBase, MenuWidget): + def __init__(self, parent, model, class_handler=None): + """Menu class. + + Used for dockable GUI components. + + :param parent: parent object + :param model: model menu data object + :param class_handler: handler object if None parent is used + """ + MenuBase.__init__(self, parent, model, class_handler) + MenuWidget.__init__(self) + + for child in self.model.root.children: + subMenu = self._createMenu(child) + self.AppendMenu(wx.ID_ANY, child.label, subMenu) + + class SearchModuleWindow(wx.Panel): """Menu tree and search widget for searching modules. diff --git a/gui/wxpython/gui_core/toolbars.py b/gui/wxpython/gui_core/toolbars.py index 3363bf32ed2..9eb64957790 100644 --- a/gui/wxpython/gui_core/toolbars.py +++ b/gui/wxpython/gui_core/toolbars.py @@ -83,7 +83,7 @@ "mapDispSettings": MetaIcon( img="monitor-settings", label=_("Map Display Settings") ), - "mapDispDocking": MetaIcon(img="monitor-dock", label=_("(Un)dock Map Display")), + "docking": MetaIcon(img="monitor-dock", label=_("(Un)dock")), } diff --git a/gui/wxpython/lmgr/frame.py b/gui/wxpython/lmgr/frame.py index b30b30c25fa..2f863103424 100644 --- a/gui/wxpython/lmgr/frame.py +++ b/gui/wxpython/lmgr/frame.py @@ -586,7 +586,7 @@ def CanCloseDisplay(askIfSaveWorkspace): return pgnum_dict return None - mapdisplay.canCloseDisplayCallback = CanCloseDisplay + mapdisplay.canCloseCallback = CanCloseDisplay # bind various events mapdisplay.BindToFrame( @@ -601,7 +601,7 @@ def CanCloseDisplay(askIfSaveWorkspace): ) mapdisplay.starting3dMode.connect(self.AddNvizTools) mapdisplay.ending3dMode.connect(self.RemoveNvizTools) - mapdisplay.closingDisplay.connect(self._closePageNoEvent) + mapdisplay.closingPage.connect(self._closePageNoEvent) # set default properties mapdisplay.SetProperties( @@ -775,9 +775,9 @@ def OnGCPManager(self, event=None, cmd=None): def OnGModeler(self, event=None, cmd=None): """Launch Graphical Modeler. See OnIClass documentation""" - from gmodeler.frame import ModelFrame + from gmodeler.frame import ModelerFrame - win = ModelFrame(parent=self, giface=self._giface) + win = ModelerFrame(parent=self, giface=self._giface) win.CentreOnScreen() win.Show() diff --git a/gui/wxpython/main_window/frame.py b/gui/wxpython/main_window/frame.py index 1eb09e73f21..9701a296d10 100644 --- a/gui/wxpython/main_window/frame.py +++ b/gui/wxpython/main_window/frame.py @@ -56,7 +56,7 @@ from gui_core.preferences import MapsetAccess, PreferencesDialog from lmgr.layertree import LayerTree, LMIcons from lmgr.menudata import LayerManagerMenuData, LayerManagerModuleTree -from main_window.notebook import MapNotebook +from main_window.notebook import MainNotebook from gui_core.widgets import GNotebook from core.gconsole import GConsole, EVT_IGNORED_CMD_RUN from core.giface import Notification @@ -69,8 +69,7 @@ MapLayersDialog, QuitDialog, ) -from gui_core.menu import SearchModuleWindow -from gui_core.menu import Menu as GMenu +from gui_core.menu import SearchModuleWindow, Menu as GMenu from core.debug import Debug from lmgr.toolbars import LMWorkspaceToolbar, LMToolsToolbar from lmgr.toolbars import LMMiscToolbar, LMNvizToolbar, DisplayPanelToolbar @@ -323,10 +322,10 @@ def SetStatusText(self, *args): """Override SbMain statusbar method""" self.statusbar.SetStatusText(*args) - def _createMapNotebook(self): + def _createMainNotebook(self): """Create Map Display notebook""" # create the notebook off-window to avoid flicker - self.mapnotebook = MapNotebook(parent=self) + self.mainnotebook = MainNotebook(parent=self) def _createDataCatalog(self, parent): """Initialize Data Catalog widget""" @@ -451,7 +450,7 @@ def CreateNewMapDisplay(giface, layertree): # create Map Display mapdisplay = MapPanel( - parent=self.mapnotebook, + parent=self.mainnotebook, giface=giface, id=wx.ID_ANY, tree=layertree, @@ -462,7 +461,7 @@ def CreateNewMapDisplay(giface, layertree): size=globalvar.MAP_WINDOW_SIZE, ) # add map display panel to notebook and make it current - self.mapnotebook.AddPage(mapdisplay, name) + self.mainnotebook.AddPage(mapdisplay, name) # set map display properties self._setUpMapDisplay(mapdisplay) @@ -515,13 +514,13 @@ def CanCloseDisplay(askIfSaveWorkspace): :return dict/None pgnum_dict/None: dict "layers" key represent map display notebook layers tree page index and - "mapnotebook" key represent + "mainnotebook" key represent map display notebook page index (single window mode) """ pgnum_dict = {} pgnum_dict["layers"] = self.notebookLayers.GetPageIndex(page) - pgnum_dict["mapnotebook"] = self.mapnotebook.GetPageIndex(mapdisplay) + pgnum_dict["mainnotebook"] = self.mainnotebook.GetPageIndex(mapdisplay) name = self.notebookLayers.GetPageText(pgnum_dict["layers"]) caption = _("Close Map Display {}").format(name) if not askIfSaveWorkspace or ( @@ -530,9 +529,7 @@ def CanCloseDisplay(askIfSaveWorkspace): return pgnum_dict return None - # set callbacks - mapdisplay.canCloseDisplayCallback = CanCloseDisplay - mapdisplay.SetDockingCallback(self.mapnotebook.UndockMapDisplay) + mapdisplay.SetUpPage(self, self.mainnotebook, CanCloseDisplay) # bind various events mapdisplay.onFocus.connect( @@ -546,7 +543,6 @@ def CanCloseDisplay(askIfSaveWorkspace): ) mapdisplay.starting3dMode.connect(self.AddNvizTools) mapdisplay.ending3dMode.connect(self.RemoveNvizTools) - mapdisplay.closingDisplay.connect(self._closePageNoEvent) # set default properties mapdisplay.SetProperties( @@ -572,7 +568,7 @@ def BuildPanes(self): self._auimgr.SetAutoNotebookTabArt(SimpleTabArt()) # initialize all main widgets self.statusbar = SbMain(parent=self, giface=self._giface) - self._createMapNotebook() + self._createMainNotebook() self._createDataCatalog(parent=self) self._createDisplay(parent=self) self._createSearchModule(parent=self) @@ -620,7 +616,7 @@ def BuildPanes(self): ) self._auimgr.AddPane( - self.mapnotebook, + self.mainnotebook, aui.AuiPaneInfo().Name("map display content").CenterPane().PaneBorder(True), ) @@ -878,11 +874,21 @@ def OnGCPManager(self, event=None, cmd=None): def OnGModeler(self, event=None, cmd=None): """Launch Graphical Modeler. See OnIClass documentation""" - from gmodeler.frame import ModelFrame + from gmodeler.panels import ModelerPanel + from gmodeler.menudata import ModelerMenuData - win = ModelFrame(parent=self, giface=self._giface) - win.CentreOnScreen() - win.Show() + gmodeler_panel = ModelerPanel( + parent=self, giface=self._giface, statusbar=self.statusbar, dockable=True + ) + gmodeler_panel.SetUpPage( + self, + self.mainnotebook, + menuModel=ModelerMenuData().GetModel(separators=True), + menuName="&Modeler", + ) + + # add map display panel to notebook and make it current + self.mainnotebook.AddPage(gmodeler_panel, _("Graphical Modeler")) def OnPsMap(self, event=None, cmd=None): """Launch Cartographic Composer. See OnIClass documentation""" @@ -984,8 +990,8 @@ def OnCBPageChanged(self, event): self.currentPage = self.notebookLayers.GetCurrentPage() self.currentPageNum = self.notebookLayers.GetSelection() - if hasattr(self.currentPage, "maptree") and self.mapnotebook.GetCurrentPage(): - self.mapnotebook.SetSelectionToMapPage(self.GetMapDisplay()) + if hasattr(self.currentPage, "maptree") and self.mainnotebook.GetCurrentPage(): + self.mainnotebook.SetSelectionToMainPage(self.GetMapDisplay()) event.Skip() @@ -1009,13 +1015,19 @@ def OnCBPageClosing(self, event): maptree = self.notebookLayers.GetPage(event.GetSelection()).maptree maptree.GetMapDisplay().CleanUp() - self.mapnotebook.DeleteMapPage(self.GetMapDisplay()) + self.mainnotebook.DeleteMainPage(self.GetMapDisplay()) maptree.Close(True) self.currentPage = None event.Skip() + def _renamePageNoEvent(self, pgnum_dict, is_docked, text): + if is_docked: + self.mainnotebook.SetMainPageText( + self.mainnotebook.GetPage(pgnum_dict["mainnotebook"]), text + ) + def _closePageNoEvent(self, pgnum_dict, is_docked): """If map display is docked, close page and destroy map display without generating layer notebook page closing event. If map display is undocked, close only @@ -1023,20 +1035,21 @@ def _closePageNoEvent(self, pgnum_dict, is_docked): :param dict pgnum_dict: dict "layers" key represent map display notebook layers tree page index and - "mapnotebook" key represent map display + "mainnotebook" key represent map display notebook page index (single window mode) boolean is_docked: "True" means that map display is docked in map display notebook, "False" means that map display is undocked to independent frame """ self.notebookLayers.Unbind(FN.EVT_FLATNOTEBOOK_PAGE_CLOSING) - self.notebookLayers.DeletePage(pgnum_dict["layers"]) + if "layers" in pgnum_dict: + self.notebookLayers.DeletePage(pgnum_dict["layers"]) self.notebookLayers.Bind( FN.EVT_FLATNOTEBOOK_PAGE_CLOSING, self.OnCBPageClosing, ) if is_docked: - self.mapnotebook.DeletePage(pgnum_dict["mapnotebook"]) + self.mainnotebook.DeletePage(pgnum_dict["mainnotebook"]) def _focusPage(self, notification): """Focus the 'Console' notebook page according to event notification.""" @@ -1174,7 +1187,7 @@ def GetAuiNotebook(self): :return: aui notebook instance """ - return self.mapnotebook + return self.mainnotebook def GetLayerNotebook(self): """Get Layers Notebook""" @@ -1717,7 +1730,7 @@ def OnRenameDisplay(self, event): if dlg.ShowModal() == wx.ID_OK: name = dlg.GetValue() self.notebookLayers.SetPageText(page=self.currentPageNum, text=name) - self.mapnotebook.SetMapPageText(page=self.GetMapDisplay(), text=name) + self.mainnotebook.SetMainPageText(page=self.GetMapDisplay(), text=name) dlg.Destroy() def OnRasterRules(self, event): diff --git a/gui/wxpython/main_window/notebook.py b/gui/wxpython/main_window/notebook.py index fdd00820cc1..77b37ada1ea 100644 --- a/gui/wxpython/main_window/notebook.py +++ b/gui/wxpython/main_window/notebook.py @@ -4,8 +4,8 @@ @brief Custom AuiNotebook class and class for undocked AuiNotebook frame Classes: - - notebook::MapPageFrame - - notebook::MapNotebook + - notebook::MainPageFrame + - notebook::MainNotebook (C) 2022 by the GRASS Development Team @@ -23,52 +23,57 @@ from core import globalvar from gui_core.wrap import SimpleTabArt +from mapdisp.frame import MapPanel -class MapPageFrame(wx.Frame): - """Frame for independent map display window.""" +class MainPageFrame(wx.Frame): + """Frame for independent window.""" - def __init__(self, parent, mapdisplay, size, pos, title): + def __init__(self, parent, panel, size, pos, title, icon="grass", menu=None): wx.Frame.__init__(self, parent=parent, size=size, pos=pos, title=title) - self.mapdisplay = mapdisplay - self.mapdisplay.Reparent(self) + self.panel = panel + self.panel.Reparent(self) self._layout() # set system icon self.SetIcon( - wx.Icon( - os.path.join(globalvar.ICONDIR, "grass_map.ico"), wx.BITMAP_TYPE_ICO - ) + wx.Icon(os.path.join(globalvar.ICONDIR, icon + ".ico"), wx.BITMAP_TYPE_ICO) ) - self.mapdisplay.onFocus.emit() + if menu is not None: + self.SetMenuBar(menu) + + self.panel.onFocus.emit() self.Bind(wx.EVT_CLOSE, self.OnClose) self._show() def _layout(self): sizer = wx.BoxSizer(wx.VERTICAL) - sizer.Add(self.mapdisplay, proportion=1, flag=wx.EXPAND) + sizer.Add(self.panel, proportion=1, flag=wx.EXPAND) self.SetSizer(sizer) self.CentreOnParent() def _show(self): - """Show frame and contained mapdisplay panel""" - self.mapdisplay.Show() + """Show frame and contained panel""" + self.panel.Show() self.Show() def SetDockingCallback(self, function): - """Set docking callback on reparented mapdisplay panel""" - self.mapdisplay.SetDockingCallback(function) + """Set docking callback on reparented panel""" + self.panel.SetDockingCallback(function) def OnClose(self, event): """Close frame and associated layer notebook page.""" - self.mapdisplay.OnCloseWindow(event=None, askIfSaveWorkspace=True) + if isinstance(self.panel, MapPanel): + self.panel.OnCloseWindow(event=None, askIfSaveWorkspace=True) + else: + self.panel.OnCloseWindow(event=None) -class MapNotebook(aui.AuiNotebook): - """Map notebook class. Overrides some AuiNotebook classes. +class MainNotebook(aui.AuiNotebook): + """Main notebook class. Overrides some AuiNotebook classes. Takes into consideration the dock/undock functionality. """ @@ -83,34 +88,62 @@ def __init__( self.SetArtProvider(SimpleTabArt()) # bindings - self.Bind( - aui.EVT_AUINOTEBOOK_PAGE_CHANGED, - lambda evt: self.GetCurrentPage().onFocus.emit(), - ) + self.Bind(aui.EVT_AUINOTEBOOK_PAGE_CHANGED, self.OnPageChanged) self.Bind(aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self.OnClose) - def UndockMapDisplay(self, page): - """Undock active map display to independent MapFrame object""" + # remember number of items in the menu + self._menuCount = self.parent.menubar.GetMenuCount() + + def OnPageChanged(self, event): + page = self.GetCurrentPage() + page.onFocus.emit() + + # set up menu + mbar = self.parent.menubar + if page.HasMenu(): + # add new (or replace if exists) additional menu item related to this page + menu, menuName = page.GetMenu() + if mbar.GetMenuCount() == self._menuCount: + appendMenu = mbar.Insert + else: + appendMenu = mbar.Replace + appendMenu(self._menuCount - 1, menu, menuName) + elif mbar.GetMenuCount() > self._menuCount: + # remove additional menu item + mbar.Remove(self._menuCount - 1) + + def UndockPage(self, page): + """Undock active page to independent MainFrame object""" index = self.GetPageIndex(page) text = self.GetPageText(index) original_size = page.GetSize() original_pos = page.GetPosition() + icon = "grass_map" if isinstance(page, MapPanel) else "grass" + if page.HasMenu(): + menu, _ = page.GetMenu() + else: + menu = None self.RemovePage(index) - frame = MapPageFrame( + frame = MainPageFrame( parent=self.parent, - mapdisplay=page, + panel=page, size=original_size, pos=original_pos, title=text, + icon=icon, + menu=menu, ) - frame.SetDockingCallback(self.DockMapDisplay) + frame.SetDockingCallback(self.DockPage) - def DockMapDisplay(self, page): - """Dock independent MapFrame object back to Aui.Notebook""" + def DockPage(self, page): + """Dock independent MainFrame object back to Aui.Notebook""" frame = page.GetParent() page.Reparent(self) - page.SetDockingCallback(self.UndockMapDisplay) + page.SetDockingCallback(self.UndockPage) self.AddPage(page, frame.GetTitle()) + if frame.GetMenuBar(): + # avoid destroying menu if defined + frame.SetMenuBar(None) frame.Destroy() def AddPage(self, *args, **kwargs): @@ -119,8 +152,8 @@ def AddPage(self, *args, **kwargs): super().AddPage(*args, **kwargs) self.SetSelection(self.GetPageCount() - 1) - def SetSelectionToMapPage(self, page): - """Decides whether to set selection to a MapNotebook page + def SetSelectionToMainPage(self, page): + """Decides whether to set selection to a MainNotebook page or an undocked independent frame""" self.SetSelection(self.GetPageIndex(page)) @@ -128,8 +161,8 @@ def SetSelectionToMapPage(self, page): frame = page.GetParent() wx.CallLater(500, lambda: frame.Raise() if frame else None) - def DeleteMapPage(self, page): - """Decides whether to delete a MapNotebook page + def DeleteMainPage(self, page): + """Decides whether to delete a MainNotebook page or close an undocked independent frame""" if page.IsDocked(): self.DeletePage(self.GetPageIndex(page)) @@ -137,8 +170,8 @@ def DeleteMapPage(self, page): frame = page.GetParent() frame.Destroy() - def SetMapPageText(self, page, text): - """Decides whether sets title to MapNotebook page + def SetMainPageText(self, page, text): + """Decides whether sets title to MainNotebook page or an undocked independent frame""" if page.IsDocked(): self.SetPageText(page_idx=self.GetPageIndex(page), text=text) @@ -149,6 +182,9 @@ def SetMapPageText(self, page, text): def OnClose(self, event): """Page of map notebook is being closed""" - display = self.GetCurrentPage() - display.OnCloseWindow(event=None, askIfSaveWorkspace=True) + page = self.GetCurrentPage() + if isinstance(page, MapPanel): + page.OnCloseWindow(event=None, askIfSaveWorkspace=True) + else: + page.OnCloseWindow(event=None) event.Veto() diff --git a/gui/wxpython/main_window/page.py b/gui/wxpython/main_window/page.py new file mode 100644 index 00000000000..a56d424dba9 --- /dev/null +++ b/gui/wxpython/main_window/page.py @@ -0,0 +1,145 @@ +""" +@package main_window.notebook + +@brief Custom AuiNotebook class and class for undocked AuiNotebook frame + +Classes: + - page::MainPageBase + +(C) 2023 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 Linda Kladivova +@author Anna Petrasova +""" + +from grass.pydispatch.signal import Signal +from gui_core.menu import MenuItem as GMenuItem, Menu as GMenu + + +class MainPageBase: + def __init__(self, dockable): + self._mainnotebook = None + + # menu(s) associated with the panel + self._menu = {} + self._menuModel = None + self._menuName = None + + self.canCloseCallback = None + + # distinquishes whether map panel is dockable (Single-Window) + self._dockable = dockable + + # distinguishes whether map panel is docked or not + self._docked = True + + # undock/dock bound method + self._docking_callback = None + + # Emitted when switching map notebook tabs (Single-Window) + self.onFocus = Signal("MainPage.onFocus") + + # Emitted when closing page by closing its window. + self.closingPage = Signal("MainPage.closingPage") + + # Emitted when renaming page. + self.renamingPage = Signal("MainPage.renamingPage") + + def _pgnumDict(self): + """Get dictionary containg page index""" + return {"mainnotebook": self._mainnotebook.GetPageIndex(self)} + + def SetUpPage( + self, parent, notebook, can_close=None, menuModel=None, menuName=None + ): + self._mainnotebook = notebook + + def CanClosePage(): + return self._pgnumDict() + + # set callbacks + self.canCloseCallback = CanClosePage if can_close is None else can_close + self.SetDockingCallback(notebook.UndockPage) + + # bind various events + self.closingPage.connect(parent._closePageNoEvent) + self.renamingPage.connect(parent._renamePageNoEvent) + + # set up menu if defined + self._menuModel = menuModel + self._menuName = menuName + + def SetDockingCallback(self, function): + """Sets docking bound method to dock or undock""" + self._docking_callback = function + + def IsDocked(self): + return self._docked + + def IsDockable(self): + return self._dockable + + def OnDockUndock(self, event=None): + """Dock or undock map display panel to independent MapFrame""" + if self._docking_callback: + self._docked = not self._docked + self._docking_callback(self) + + def _onCloseWindow(self, event): + """Close window""" + if self.canCloseCallback: + pgnum_dict = self.canCloseCallback() + if pgnum_dict is not None: + if self.IsDockable(): + self.closingPage.emit( + pgnum_dict=pgnum_dict, is_docked=self.IsDocked() + ) + if not self.IsDocked(): + frame = self.GetParent() + frame.Destroy() + else: + self.closingPage.emit(pgnum_dict=pgnum_dict) + # Destroy is called when notebook page is deleted + else: + self.parent.Destroy() + + def RenamePage(self, title): + """Rename page or change frame title""" + if self.canCloseCallback: + pgnum_dict = self._pgnumDict() + if pgnum_dict is not None: + if self.IsDockable(): + self.renamingPage.emit( + pgnum_dict=pgnum_dict, is_docked=self.IsDocked(), text=title + ) + if not self.IsDocked(): + self.GetParent().SetTitle(title) + else: + self.GetParent().SetTitle(title) + + def HasMenu(self): + """Check if menu is defined. + + :return True if menu defined otherwise False + """ + return self._menuModel is not None + + def GetMenu(self): + """Get menu object if defined. + + :return: menu object (Menu for undocked window, MenuItem for docked window) + """ + menu = None + if self._menuModel is not None: + if self._docked not in self._menu: + menuClass = GMenuItem if self._docked else GMenu + menu = self._menu[self._docked] = menuClass( + parent=self.parent, model=self._menuModel, class_handler=self + ) + else: + menu = self._menu[self._docked] + + return menu, self._menuName diff --git a/gui/wxpython/mapdisp/frame.py b/gui/wxpython/mapdisp/frame.py index 65a7f358b52..6e10937ec6d 100644 --- a/gui/wxpython/mapdisp/frame.py +++ b/gui/wxpython/mapdisp/frame.py @@ -56,13 +56,14 @@ from gui_core.vselect import VectorSelectBase, VectorSelectHighlighter from gui_core.wrap import Menu from mapdisp import statusbar as sb +from main_window.page import MainPageBase import grass.script as grass from grass.pydispatch.signal import Signal -class MapPanel(SingleMapPanel): +class MapPanel(SingleMapPanel, MainPageBase): """Main panel for map display window. Drawing takes place in child double buffered drawing window. """ @@ -103,6 +104,7 @@ def __init__( name=name, **kwargs, ) + MainPageBase.__init__(self, dockable) self._giface = giface # Layer Manager object @@ -111,24 +113,10 @@ def __init__( # Layer Manager layer tree object # used for VDigit toolbar and window and GLWindow self.tree = tree - # checks for saving workspace - self.canCloseDisplayCallback = None - - # distinquishes whether map panel is dockable (Single-Window) - self._dockable = dockable - - # distinguishes whether map panel is docked or not - self._docked = True - - # undock/dock bound method - self._docking_callback = None # Saved Map Display output img size self._saved_output_img_size = None - # Emitted when switching map notebook tabs (Single-Window) - self.onFocus = Signal("MapPanel.onFocus") - # Emitted when starting (switching to) 3D mode. # Parameter firstTime specifies if 3D was already activated. self.starting3dMode = Signal("MapPanel.starting3dMode") @@ -136,9 +124,6 @@ def __init__( # Emitted when ending (switching from) 3D mode. self.ending3dMode = Signal("MapPanel.ending3dMode") - # Emitted when closing display by closing its window. - self.closingDisplay = Signal("MapPanel.closingDisplay") - # Emitted when closing display by closing its window. self.closingVNETDialog = Signal("MapPanel.closingVNETDialog") @@ -677,22 +662,6 @@ def RemoveQueryLayer(self): for layer in qlayer: self.GetMap().DeleteLayer(layer) - def SetDockingCallback(self, function): - """Sets docking bound method to dock or undock""" - self._docking_callback = function - - def OnDockUndock(self, event=None): - """Dock or undock map display panel to independent MapFrame""" - if self._docking_callback: - self._docking_callback(self) - self._docked = not self._docked - - def IsDocked(self): - return self._docked - - def IsDockable(self): - return self._dockable - def OnRender(self, event): """Re-render map composition (each map layer)""" self.RemoveQueryLayer() @@ -1001,22 +970,20 @@ def OnCloseWindow(self, event, askIfSaveWorkspace=True): Also close associated layer tree page """ Debug.msg(2, "MapPanel.OnCloseWindow()") - if self.canCloseDisplayCallback: - pgnum_dict = self.canCloseDisplayCallback( - askIfSaveWorkspace=askIfSaveWorkspace - ) + if self.canCloseCallback: + pgnum_dict = self.canCloseCallback(askIfSaveWorkspace=askIfSaveWorkspace) if pgnum_dict is not None: self.CleanUp() if pgnum_dict["layers"] > -1: if self.IsDockable(): - self.closingDisplay.emit( + self.closingPage.emit( pgnum_dict=pgnum_dict, is_docked=self.IsDocked() ) if not self.IsDocked(): frame = self.GetParent() frame.Destroy() else: - self.closingDisplay.emit(pgnum_dict=pgnum_dict) + self.closingPage.emit(pgnum_dict=pgnum_dict) # Destroy is called when notebook page is deleted else: self.CleanUp() diff --git a/gui/wxpython/mapdisp/toolbars.py b/gui/wxpython/mapdisp/toolbars.py index cfb42bebc33..3da61d60398 100644 --- a/gui/wxpython/mapdisp/toolbars.py +++ b/gui/wxpython/mapdisp/toolbars.py @@ -253,8 +253,8 @@ def _toolbarData(self): if self.parent.IsDockable(): data = data + ( ( - ("mapDispDocking", BaseIcons["mapDispDocking"].label), - BaseIcons["mapDispDocking"], + ("docking", BaseIcons["docking"].label), + BaseIcons["docking"], self.parent.OnDockUndock, wx.ITEM_CHECK, ), diff --git a/gui/wxpython/xml/menudata_modeler.xml b/gui/wxpython/xml/menudata_modeler.xml index bf2bd61fe62..cf6d1b3e444 100644 --- a/gui/wxpython/xml/menudata_modeler.xml +++ b/gui/wxpython/xml/menudata_modeler.xml @@ -7,19 +7,19 @@ Create new model OnModelNew - Ctrl+N + Ctrl+Alt+N Load model from file OnModelOpen - Ctrl+O + Ctrl+Alt+O Save model OnModelSave - Ctrl+S + Ctrl+Alt+S @@ -41,14 +41,14 @@ Export model to Python script OnExportPython - Ctrl+P + Ctrl+Alt+P Close modeler window OnCloseWindow - Ctrl+W + Ctrl+Alt+W @@ -69,13 +69,13 @@ Add action (GRASS command) to model OnAddAction - Ctrl+A + Ctrl+Alt+A Add data item to model OnAddData - Ctrl+D + Ctrl+Alt+D @@ -86,19 +86,19 @@ Adds loop (series) to model OnDefineLoop - Ctrl+L + Ctrl+Alt+L Adds condition (if/else) to model OnDefineCondition - Ctrl+I + Ctrl+Alt+I Adds comment to model OnAddComment - Ctrl+# + Ctrl+Alt+# @@ -122,7 +122,7 @@ Run entire model OnRunModel - Ctrl+R + Ctrl+Alt+R