From a23851c3cf6de94a33a148baa6ecfa2a2be71ce1 Mon Sep 17 00:00:00 2001 From: Martin Landa Date: Sun, 4 Jun 2023 11:39:33 +0200 Subject: [PATCH 01/51] first step, split 'frame' into 'panel' and 'canvas' modules --- gui/wxpython/gmodeler/canvas.py | 486 ++++++ gui/wxpython/gmodeler/frame.py | 1932 ++--------------------- gui/wxpython/gmodeler/g.gui.gmodeler.py | 4 +- gui/wxpython/gmodeler/panel.py | 1227 ++++++++++++++ 4 files changed, 1862 insertions(+), 1787 deletions(-) create mode 100644 gui/wxpython/gmodeler/canvas.py create mode 100644 gui/wxpython/gmodeler/panel.py diff --git a/gui/wxpython/gmodeler/canvas.py b/gui/wxpython/gmodeler/canvas.py new file mode 100644 index 00000000000..4256f25513b --- /dev/null +++ b/gui/wxpython/gmodeler/canvas.py @@ -0,0 +1,486 @@ +""" +@package gmodeler.canvas + +@brief wxGUI Graphical Modeler for creating, editing, and managing models + +Classes: + - frame::ModelCanvas + - frame::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 + +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): + """Determine optimal position for newly added object + + :return: x,y + """ + xNew, yNew = map(lambda x: x / 2, self.GetSize()) + diagram = self.GetDiagram() + + for shape in diagram.GetShapeList(): + y = shape.GetY() + yBox = shape.GetBoundingBoxMin()[1] / 2 + if yBox > 0 and y < yNew + yBox and y > yNew - yBox: + yNew += yBox * 3 + + return xNew, yNew + + 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"): + 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() diff --git a/gui/wxpython/gmodeler/frame.py b/gui/wxpython/gmodeler/frame.py index 8ce9a716e82..474a1549a8d 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. @@ -22,92 +17,30 @@ import os import sys -import time -import stat -import tempfile -import random -import six 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 - - -class ModelFrame(wx.Frame): +from gmodeler.panel import ModelerPanel + +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) ) @@ -123,337 +56,16 @@ def __init__( self.SetToolBar(self.toolbar) self.statusbar = self.CreateStatusBar(number=1) - - 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) - 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_CLOSE, self.OnCloseWindow) - self.Bind(wx.EVT_SIZE, self.OnSize) - self.notebook.Bind(FN.EVT_FLATNOTEBOOK_PAGE_CHANGED, self.OnPageChanged) - - self._layout() + + self.Panel = ModelerPanel(parent=self, giface=giface, statusbar=self.statusbar) 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.OnWorkspaceSaveAs() - 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 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 _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() + self.Bind(wx.EVT_CLOSE, self.OnCloseWindow) + # TODO + self.modelChanged = None + def OnModelNew(self, event): """Create new model""" Debug.msg(4, "ModelFrame.OnModelNew():") @@ -492,20 +104,10 @@ def OnModelNew(self, event): # no model file loaded self.modelFile = None - self.modelChanged = False + # TODO + # 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 = "" @@ -646,63 +248,6 @@ def OnRunModel(self, event): 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 @@ -771,58 +316,57 @@ def OnExportPython(self, event=None, text=None): 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 + 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?" + ) - .. todo:: - move to ModelCanvas? - """ - self.ModelChanged() + # 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.OnWorkspaceSaveAs() + else: + self.WriteModelFile(self.modelFile) + elif ret == wx.ID_CANCEL: + dlg.Destroy() + return + dlg.Destroy() - 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.Destroy() - self._addEvent(loop) - self.model.AddItem(loop) + def OnPreferences(self, event): + """Open preferences dialog""" + dlg = PreferencesDialog(parent=self, giface=self._giface) + dlg.CenterOnParent() + dlg.Show() 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() + 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() @@ -946,1306 +490,124 @@ def OnAddComment(self, event): 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 - width, height = self.canvas.GetSize() - x = width / 2 - 200 + self._randomShift() - y = height / 2 + self._randomShift() - for p in params["params"]: - if p.get("prompt", "") not in ( - "raster", - "vector", - "raster_3d", - "dbtable", - ): - 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 - - data = ModelData( - self, - value=p.get("value", ""), - prompt=p.get("prompt", ""), - x=x, - y=y, - ) - self._addEvent(data) - self.canvas.diagram.AddShape(data) - data.Show(True) - - 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) - - self.canvas.Refresh() - - if dcmd: - layer.SetProperties(params, propwin) - - 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 OnDefineRelation(self, event): + """Define relation between data and action items""" + self.canvas.SetCursor(self.cursors["cross"]) + self.defineRelation = {"from": None, "to": None} - def WriteModelFile(self, filename): - """Save model to model file, recover original file on error. + def OnDefineLoop(self, event): + """Define new loop in the model - :return: True on success - :return: False on failure + .. todo:: + move to ModelCanvas? """ - 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 IOError: - 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 + self.ModelChanged() - # 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) + width, height = self.canvas.GetSize() + loop = ModelLoop( + self, x=width / 2, y=height / 2, id=self.model.GetNumItems() + 1 ) - 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 + self.canvas.diagram.AddShape(loop) + loop.Show(True) - 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._addEvent(loop) + self.model.AddItem(loop) self.canvas.Refresh() + def OnDefineCondition(self, event): + """Define new condition in the model -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): - """Determine optimal position for newly added object - - :return: x,y + .. todo:: + move to ModelCanvas? """ - xNew, yNew = map(lambda x: x / 2, self.GetSize()) - diagram = self.GetDiagram() - - for shape in diagram.GetShapeList(): - y = shape.GetY() - yBox = shape.GetBoundingBoxMin()[1] / 2 - if yBox > 0 and y < yNew + yBox and y > yNew - yBox: - yNew += yBox * 3 - - return xNew, yNew - - 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"): - 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() + self.ModelChanged() - 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), + width, height = self.canvas.GetSize() + cond = ModelCondition( + self, x=width / 2, y=height / 2, id=self.model.GetNumItems() + 1 ) - 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) + self.canvas.diagram.AddShape(cond) + cond.Show(True) - 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) + self._addEvent(cond) + self.model.AddItem(cond) - except GException as e: - GError(parent=self, message="{}".format(e)) + self.canvas.Refresh() - def OnRemove(self, event): + def OnRemoveItem(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) + self.GetCanvas().RemoveSelected() - 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() + 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"]) - 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"), - ) + dlg.Destroy() - self.list = ItemListCtrl( - parent=self, - columns=[_("Label"), _("In loop"), _("Parameterized"), _("Command")], - columnsNotEditable=[1, 2, 3], - frame=self.parent, - ) + def OnDeleteData(self, event): + """Delete intermediate data""" + rast, vect, rast3d, msg = self.model.GetIntermediateData() - 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: + if not rast and not vect and not rast3d: + GMessage(parent=self, message=_("No intermediate data to delete.")) 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+") - self.write_object(fd, self.parent.GetModel()) - 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( + dlg = wx.MessageDialog( 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, + message=_("Do you want to permanently delete data?%s" % msg), + caption=_("Delete intermediate data?"), + style=wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION, ) - if dlg.ShowModal() == wx.ID_OK: - filename = dlg.GetPath() + ret = dlg.ShowModal() + dlg.Destroy() + if ret == wx.ID_YES: + self._deleteIntermediateData() - if not filename: - return "" + 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 - # check for extension - if filename[-3:] != ".py": - filename += ".py" + self.SetStatusText(_("Validating model..."), 0) + errList = self.model.Validate() + self.SetStatusText("", 0) - 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 errList: + GWarning( + parent=self, message=_("Model is not valid.\n\n%s") % "\n".join(errList) ) - 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 IOError 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, - ) + GMessage(parent=self, message=_("Model is valid.")) - 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 OnHelp(self, event): + """Show help""" + self._giface.Help(entry="wxGUI.gmodeler") - 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 OnAbout(self, event): + """Display About window""" + ShowAboutDialog(prgName=_("wxGUI Graphical Modeler"), startYear="2010") - def OnSaveAs(self, event): - """Save the script to a file.""" - self.SaveAs(force=False) - event.Skip() + def OnCanvasRefresh(self, event): + """Refresh canvas""" + self.SetStatusText(_("Redrawing model..."), 0) + self.GetCanvas().Refresh() + self.SetStatusText("", 0) - def IsModified(self): - """Check if the script has been modified.""" - return self.body.modified + def OnVariables(self, event): + """Switch to variables page""" + self.notebook.SetSelectionByName("variables") - 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..d612c569230 100755 --- a/gui/wxpython/gmodeler/g.gui.gmodeler.py +++ b/gui/wxpython/gmodeler/g.gui.gmodeler.py @@ -47,10 +47,10 @@ 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"), diff --git a/gui/wxpython/gmodeler/panel.py b/gui/wxpython/gmodeler/panel.py new file mode 100644 index 00000000000..99ce4f6a0f5 --- /dev/null +++ b/gui/wxpython/gmodeler/panel.py @@ -0,0 +1,1227 @@ +""" +@package gmodeler.frame + +@brief wxGUI Graphical Modeler for creating, editing, and managing models + +Classes: + - frame::ModelerPanel + - frame::VariablePanel + - frame::ItemPanel + - frame::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 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 gui_core.forms import GUI +from gmodeler.preferences import PreferencesDialog, PropertiesDialog +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 gmodeler.canvas import ModelCanvas +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 + +class ModelerPanel(wx.Panel): + def __init__( + self, parent, giface, id=wx.ID_ANY, title=_("Graphical Modeler"), statusbar=None, **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) + self.SetName("Modeler") + + 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) + 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.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 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 + + # TODO + # 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 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""" + 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 + width, height = self.canvas.GetSize() + x = width / 2 - 200 + self._randomShift() + y = height / 2 + self._randomShift() + for p in params["params"]: + if p.get("prompt", "") not in ( + "raster", + "vector", + "raster_3d", + "dbtable", + ): + 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 + + data = ModelData( + self, + value=p.get("value", ""), + prompt=p.get("prompt", ""), + x=x, + y=y, + ) + self._addEvent(data) + self.canvas.diagram.AddShape(data) + data.Show(True) + + 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) + + self.canvas.Refresh() + + if dcmd: + layer.SetProperties(params, propwin) + + 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 + # TODO + # 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 IOError: + 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 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+") + self.write_object(fd, self.parent.GetModel()) + 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 IOError 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 From 10809a9d181629010fece978ae9bf83b03174ad1 Mon Sep 17 00:00:00 2001 From: Martin Landa Date: Sun, 4 Jun 2023 15:02:30 +0200 Subject: [PATCH 02/51] gmodeler dockable --- gui/wxpython/gmodeler/canvas.py | 4 +- gui/wxpython/gmodeler/frame.py | 568 +---------------- gui/wxpython/gmodeler/{panel.py => panels.py} | 575 +++++++++++++++++- gui/wxpython/main_window/frame.py | 9 +- 4 files changed, 575 insertions(+), 581 deletions(-) rename gui/wxpython/gmodeler/{panel.py => panels.py} (69%) diff --git a/gui/wxpython/gmodeler/canvas.py b/gui/wxpython/gmodeler/canvas.py index 4256f25513b..b442e4f28aa 100644 --- a/gui/wxpython/gmodeler/canvas.py +++ b/gui/wxpython/gmodeler/canvas.py @@ -4,8 +4,8 @@ @brief wxGUI Graphical Modeler for creating, editing, and managing models Classes: - - frame::ModelCanvas - - frame::ModelEvtHandler + - canvas::ModelCanvas + - canvas::ModelEvtHandler (C) 2010-2023 by the GRASS Development Team diff --git a/gui/wxpython/gmodeler/frame.py b/gui/wxpython/gmodeler/frame.py index 474a1549a8d..f30e85915f2 100644 --- a/gui/wxpython/gmodeler/frame.py +++ b/gui/wxpython/gmodeler/frame.py @@ -24,8 +24,7 @@ from gui_core.menu import Menu as Menubar from gmodeler.menudata import ModelerMenuData -from gmodeler.toolbars import ModelerToolbar -from gmodeler.panel import ModelerPanel +from gmodeler.panels import ModelerPanel class ModelerFrame(wx.Frame): def __init__( @@ -45,569 +44,14 @@ def __init__( wx.Icon(os.path.join(globalvar.ICONDIR, "grass.ico"), wx.BITMAP_TYPE_ICO) ) + self.statusbar = self.CreateStatusBar(number=1) + self.panel = ModelerPanel(parent=self, giface=giface, statusbar=self.statusbar) + self.menubar = Menubar( - parent=self, model=ModelerMenuData().GetModel(separators=True) + parent=self.panel, 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.SetName("ModelerFrame") self.SetMinSize((640, 300)) self.SetSize((800, 600)) - - self.Bind(wx.EVT_CLOSE, self.OnCloseWindow) - - # TODO - self.modelChanged = None - - 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 - # TODO - # self.modelChanged = False - self.SetTitle(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, "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(None) - - def OnModelSaveAs(self, event): - """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 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 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.OnWorkspaceSaveAs() - else: - self.WriteModelFile(self.modelFile) - elif ret == wx.ID_CANCEL: - dlg.Destroy() - return - dlg.Destroy() - - self.Destroy() - - 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 + self._randomShift(), - y=y + self._randomShift(), - 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: - cmdLength = len(action.GetLog(string=False)) - if cmdLength > 1 and action.IsValid(): - self.GetOptData( - dcmd=action.GetLog(string=False), - layer=action, - params=action.GetParams(), - propwin=None, - ) - else: - 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 + self._randomShift(), - y=y + self._randomShift(), - 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") - - - diff --git a/gui/wxpython/gmodeler/panel.py b/gui/wxpython/gmodeler/panels.py similarity index 69% rename from gui/wxpython/gmodeler/panel.py rename to gui/wxpython/gmodeler/panels.py index 99ce4f6a0f5..29aa698651e 100644 --- a/gui/wxpython/gmodeler/panel.py +++ b/gui/wxpython/gmodeler/panels.py @@ -4,10 +4,10 @@ @brief wxGUI Graphical Modeler for creating, editing, and managing models Classes: - - frame::ModelerPanel - - frame::VariablePanel - - frame::ItemPanel - - frame::PythonPanel + - panels::ModelerPanel + - panels::VariablePanel + - panels::ItemPanel + - panels::PythonPanel (C) 2010-2023 by the GRASS Development Team @@ -39,23 +39,19 @@ 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 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 core.settings import UserSettings from gui_core.forms import GUI -from gmodeler.preferences import PreferencesDialog, PropertiesDialog -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 gmodeler.canvas import ModelCanvas from gui_core.wrap import ( Button, EmptyBitmap, @@ -70,6 +66,13 @@ ) from gui_core.wrap import TextEntryDialog as wxTextEntryDialog +from gmodeler.giface import GraphicalModelerGrassInterface +from gmodeler.model import * +from gmodeler.dialogs import * +from gmodeler.canvas import ModelCanvas +from gmodeler.toolbars import ModelerToolbar +from gmodeler.preferences import PreferencesDialog, PropertiesDialog + wxModelDone, EVT_MODEL_DONE = NewEvent() from grass.script.utils import try_remove @@ -106,6 +109,8 @@ def __init__( wx.Panel.__init__(self, parent=parent, id=id, **kwargs) self.SetName("Modeler") + self.toolbar = ModelerToolbar(parent=self) + self.notebook = GNotebook(parent=self, style=globalvar.FNPageDStyle) self.canvas = ModelCanvas(self) @@ -176,6 +181,7 @@ 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) @@ -728,7 +734,550 @@ def DefineCondition(self, condition): parent = item self.canvas.Refresh() + + 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 + # TODO + # self.modelChanged = False + self.SetTitle(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, "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(None) + + def OnModelSaveAs(self, event): + """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 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 + self._randomShift(), + y=y + self._randomShift(), + 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: + cmdLength = len(action.GetLog(string=False)) + if cmdLength > 1 and action.IsValid(): + self.GetOptData( + dcmd=action.GetLog(string=False), + layer=action, + params=action.GetParams(), + propwin=None, + ) + else: + 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 + self._randomShift(), + y=y + self._randomShift(), + 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.OnWorkspaceSaveAs() + else: + self.WriteModelFile(self.modelFile) + elif ret == wx.ID_CANCEL: + dlg.Destroy() + return + dlg.Destroy() + if self.parent.GetName() == "ModelerFrame": + self.parent.Destroy() class VariablePanel(wx.Panel): def __init__(self, parent, id=wx.ID_ANY, **kwargs): diff --git a/gui/wxpython/main_window/frame.py b/gui/wxpython/main_window/frame.py index eee07982bf4..24dceb98dd1 100644 --- a/gui/wxpython/main_window/frame.py +++ b/gui/wxpython/main_window/frame.py @@ -835,11 +835,12 @@ 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 - win = ModelFrame(parent=self, giface=self._giface) - win.CentreOnScreen() - win.Show() + gmodeler_panel = ModelerPanel(parent=self, giface=self._giface) + + # add map display panel to notebook and make it current + self.mapnotebook.AddPage(gmodeler_panel, _("Graphical Modeler")) def OnPsMap(self, event=None, cmd=None): """Launch Cartographic Composer. See OnIClass documentation""" From 04306ce06a904605b9abeadd280ecbcebda7ccb4 Mon Sep 17 00:00:00 2001 From: Martin Landa Date: Mon, 5 Jun 2023 11:46:41 +0200 Subject: [PATCH 03/51] fix menu, WIP --- gui/wxpython/gmodeler/canvas.py | 3 ++ gui/wxpython/gmodeler/frame.py | 2 +- gui/wxpython/gmodeler/g.gui.gmodeler.py | 4 +- gui/wxpython/gmodeler/model.py | 10 +++-- gui/wxpython/gmodeler/panels.py | 58 +++++++++++++++++-------- gui/wxpython/main_window/notebook.py | 16 +++++-- gui/wxpython/xml/menudata_modeler.xml | 44 +++++++++---------- 7 files changed, 87 insertions(+), 50 deletions(-) diff --git a/gui/wxpython/gmodeler/canvas.py b/gui/wxpython/gmodeler/canvas.py index b442e4f28aa..b7abafcca5e 100644 --- a/gui/wxpython/gmodeler/canvas.py +++ b/gui/wxpython/gmodeler/canvas.py @@ -19,6 +19,9 @@ import wx from wx.lib import ogl +from gmodeler.model import * +from gmodeler.dialogs import * + class ModelCanvas(ogl.ShapeCanvas): """Canvas where model is drawn""" diff --git a/gui/wxpython/gmodeler/frame.py b/gui/wxpython/gmodeler/frame.py index f30e85915f2..cfc15897acf 100644 --- a/gui/wxpython/gmodeler/frame.py +++ b/gui/wxpython/gmodeler/frame.py @@ -48,7 +48,7 @@ def __init__( self.panel = ModelerPanel(parent=self, giface=giface, statusbar=self.statusbar) self.menubar = Menubar( - parent=self.panel, model=ModelerMenuData().GetModel(separators=True) + parent=self, model=ModelerMenuData().GetModel(separators=True) ) self.SetMenuBar(self.menubar) diff --git a/gui/wxpython/gmodeler/g.gui.gmodeler.py b/gui/wxpython/gmodeler/g.gui.gmodeler.py index d612c569230..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 @@ -56,7 +56,7 @@ def main(): 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 05f0f4ecbfc..d0964c0e202 100644 --- a/gui/wxpython/gmodeler/model.py +++ b/gui/wxpython/gmodeler/model.py @@ -58,6 +58,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 @@ -327,10 +328,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 diff --git a/gui/wxpython/gmodeler/panels.py b/gui/wxpython/gmodeler/panels.py index 29aa698651e..570b82c2d25 100644 --- a/gui/wxpython/gmodeler/panels.py +++ b/gui/wxpython/gmodeler/panels.py @@ -43,7 +43,7 @@ from core.debug import Debug from core.gcmd import GMessage, GException, GWarning, GError from core.settings import UserSettings -from core.giface import Notification +from core.giface import Notification, StandaloneGrassInterface from gui_core.widgets import GNotebook from gui_core.goutput import GConsoleWindow @@ -65,11 +65,11 @@ IsDark, ) from gui_core.wrap import TextEntryDialog as wxTextEntryDialog - +from gui_core.mapdisp import FrameMixin from gmodeler.giface import GraphicalModelerGrassInterface from gmodeler.model import * from gmodeler.dialogs import * -from gmodeler.canvas import ModelCanvas +from gmodeler.canvas import ModelCanvas, ModelEvtHandler from gmodeler.toolbars import ModelerToolbar from gmodeler.preferences import PreferencesDialog, PropertiesDialog @@ -77,7 +77,11 @@ from grass.script.utils import try_remove from grass.script import core as grass +from grass.pydispatch.signal import Signal +# TODO +# FrameMixin fails with FrameMixin.Show() takes 1 positional argument but 2 weregiven +#class ModelerPanel(FrameMixin, wx.Panel): class ModelerPanel(wx.Panel): def __init__( self, parent, giface, id=wx.ID_ANY, title=_("Graphical Modeler"), statusbar=None, **kwargs @@ -177,6 +181,9 @@ def __init__( if self.goutput: self.goutput.SetSashPosition(int(self.GetSize()[1] * 0.75)) + # TODO: + self.onFocus = Signal("ModelerPanel.onFocus") + def _layout(self): """Do layout""" sizer = wx.BoxSizer(wx.VERTICAL) @@ -201,6 +208,20 @@ def _randomShift(self): """Returns random value to shift layout""" return random.randint(-self.randomness, self.randomness) + # TODO: overwrites FrameMixin + def SetTitle(self, title): + if isinstance(self._giface, StandaloneGrassInterface): + self.parent.SetTitle(title) + + # TODO: To be moved to FrameMixin? + def SetStatusText(self, *args): + if isinstance(self._giface, StandaloneGrassInterface): + self.GetParent().SetStatusText(*args) + else: + # TODO: how to? + # self.SetStatusText(*args) + pass + def GetStatusBar(self): """Get statusbar""" return self.statusbar @@ -217,16 +238,15 @@ def ModelChanged(self, changed=True): """Update window title""" self.modelChanged = changed - # TODO - # 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) + 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""" @@ -556,8 +576,7 @@ def LoadModelFile(self, filename): return self.modelFile = filename - # TODO - # self.SetTitle(self.baseTitle + " - " + os.path.basename(self.modelFile)) + self.SetTitle(self.baseTitle + " - " + os.path.basename(self.modelFile)) self.SetStatusText(_("Please wait, loading model..."), 0) @@ -773,8 +792,7 @@ def OnModelNew(self, event): # no model file loaded self.modelFile = None - # TODO - # self.modelChanged = False + self.modelChanged = False self.SetTitle(self.baseTitle) def OnModelOpen(self, event): @@ -1276,8 +1294,12 @@ def OnCloseWindow(self, event): return dlg.Destroy() - if self.parent.GetName() == "ModelerFrame": + # TODO: StandaloneGrassInterface -> _docked ??? + if isinstance(self._giface, StandaloneGrassInterface): self.parent.Destroy() + else: + # TODO: why the page is not removed completely + self.Destroy() class VariablePanel(wx.Panel): def __init__(self, parent, id=wx.ID_ANY, **kwargs): diff --git a/gui/wxpython/main_window/notebook.py b/gui/wxpython/main_window/notebook.py index fdd00820cc1..9cc313a80f4 100644 --- a/gui/wxpython/main_window/notebook.py +++ b/gui/wxpython/main_window/notebook.py @@ -23,7 +23,7 @@ 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.""" @@ -82,6 +82,8 @@ def __init__( self.SetArtProvider(SimpleTabArt()) + self.currentDisplay = None + # bindings self.Bind( aui.EVT_AUINOTEBOOK_PAGE_CHANGED, @@ -118,6 +120,8 @@ def AddPage(self, *args, **kwargs): Adds page to notebook and makes it current""" super().AddPage(*args, **kwargs) self.SetSelection(self.GetPageCount() - 1) + if isinstance(args[0], MapPanel): + self.currentDisplay = self.GetCurrentPage() def SetSelectionToMapPage(self, page): """Decides whether to set selection to a MapNotebook page @@ -149,6 +153,12 @@ 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 page == self.currentDisplay: + page.OnCloseWindow(event=None, askIfSaveWorkspace=True) + else: + page.OnCloseWindow(event=None) event.Veto() + + def GetCurrentDisplay(self): + return self.currentDisplay diff --git a/gui/wxpython/xml/menudata_modeler.xml b/gui/wxpython/xml/menudata_modeler.xml index bf2bd61fe62..7c909a9ee1e 100644 --- a/gui/wxpython/xml/menudata_modeler.xml +++ b/gui/wxpython/xml/menudata_modeler.xml @@ -6,48 +6,48 @@ Create new model - OnModelNew + panel.OnModelNew Ctrl+N Load model from file - OnModelOpen + panel.OnModelOpen Ctrl+O Save model - OnModelSave + panel.OnModelSave Ctrl+S Save model to file - OnModelSaveAs + panel.OnModelSaveAs Close model file - OnModelClose + panel.OnModelClose Export model to image - OnExportImage + panel.OnExportImage Export model to Python script - OnExportPython + panel.OnExportPython Ctrl+P Close modeler window - OnCloseWindow + panel.OnCloseWindow Ctrl+W @@ -58,7 +58,7 @@ Modeler settings - OnPreferences + panel.OnPreferences @@ -68,66 +68,66 @@ Add action (GRASS command) to model - OnAddAction + panel.OnAddAction Ctrl+A Add data item to model - OnAddData + panel.OnAddData Ctrl+D Define relation between data and action items - OnDefineRelation + panel.OnDefineRelation Adds loop (series) to model - OnDefineLoop + panel.OnDefineLoop Ctrl+L Adds condition (if/else) to model - OnDefineCondition + panel.OnDefineCondition Ctrl+I Adds comment to model - OnAddComment + panel.OnAddComment Ctrl+# Remove action/data from model - OnRemoveItem + panel.OnRemoveItem Model properties (name, purpose, etc.) - OnModelProperties + panel.OnModelProperties Delete intermediate data defined in the model - OnDeleteData + panel.OnDeleteData Run entire model - OnRunModel + panel.OnRunModel Ctrl+R Validate entire model - OnValidateModel + panel.OnValidateModel @@ -137,13 +137,13 @@ Display the HTML man pages of Graphical modeler - OnHelp + panel.OnHelp Display information about Graphical Modeler - OnAbout + panel.OnAbout From f6545f31d1d14c9c80a35aa73e049ae19e4a7f51 Mon Sep 17 00:00:00 2001 From: Martin Landa Date: Mon, 5 Jun 2023 17:53:19 +0200 Subject: [PATCH 04/51] black applied --- gui/wxpython/gmodeler/panels.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/gui/wxpython/gmodeler/panels.py b/gui/wxpython/gmodeler/panels.py index 570b82c2d25..33924d8804b 100644 --- a/gui/wxpython/gmodeler/panels.py +++ b/gui/wxpython/gmodeler/panels.py @@ -56,16 +56,12 @@ Button, EmptyBitmap, ImageFromBitmap, - Menu, - NewId, StaticBox, StaticText, StockCursor, TextCtrl, IsDark, ) -from gui_core.wrap import TextEntryDialog as wxTextEntryDialog -from gui_core.mapdisp import FrameMixin from gmodeler.giface import GraphicalModelerGrassInterface from gmodeler.model import * from gmodeler.dialogs import * @@ -79,12 +75,16 @@ from grass.script import core as grass from grass.pydispatch.signal import Signal -# TODO -# FrameMixin fails with FrameMixin.Show() takes 1 positional argument but 2 weregiven -#class ModelerPanel(FrameMixin, wx.Panel): + class ModelerPanel(wx.Panel): def __init__( - self, parent, giface, id=wx.ID_ANY, title=_("Graphical Modeler"), statusbar=None, **kwargs + self, + parent, + giface, + id=wx.ID_ANY, + title=_("Graphical Modeler"), + statusbar=None, + **kwargs, ): """Graphical modeler main panel :param parent: parent window @@ -208,14 +208,14 @@ def _randomShift(self): """Returns random value to shift layout""" return random.randint(-self.randomness, self.randomness) - # TODO: overwrites FrameMixin + # TODO: def SetTitle(self, title): if isinstance(self._giface, StandaloneGrassInterface): self.parent.SetTitle(title) - # TODO: To be moved to FrameMixin? - def SetStatusText(self, *args): - if isinstance(self._giface, StandaloneGrassInterface): + # TODO: + def SetStatusText(self, *args): + if isinstance(self._giface, StandaloneGrassInterface): self.GetParent().SetStatusText(*args) else: # TODO: how to? @@ -225,7 +225,7 @@ def SetStatusText(self, *args): def GetStatusBar(self): """Get statusbar""" return self.statusbar - + def GetCanvas(self): """Get canvas""" return self.canvas @@ -753,7 +753,7 @@ def DefineCondition(self, condition): parent = item self.canvas.Refresh() - + def OnModelNew(self, event): """Create new model""" Debug.msg(4, "ModelFrame.OnModelNew():") @@ -1301,6 +1301,7 @@ def OnCloseWindow(self, event): # TODO: why the page is not removed completely self.Destroy() + class VariablePanel(wx.Panel): def __init__(self, parent, id=wx.ID_ANY, **kwargs): """Manage model variables panel""" From b7096fc67c6b52a68be4d4460555023c415a9755 Mon Sep 17 00:00:00 2001 From: Martin Landa Date: Mon, 6 Nov 2023 12:47:39 +0100 Subject: [PATCH 05/51] merge manually #3188 --- gui/wxpython/gmodeler/panels.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gui/wxpython/gmodeler/panels.py b/gui/wxpython/gmodeler/panels.py index 33924d8804b..97cd9258ddf 100644 --- a/gui/wxpython/gmodeler/panels.py +++ b/gui/wxpython/gmodeler/panels.py @@ -849,9 +849,9 @@ def OnModelSave(self, event=None): self.SetStatusText(_("File <%s> saved") % self.modelFile, 0) self.SetTitle(self.baseTitle + " - " + os.path.basename(self.modelFile)) elif not self.modelFile: - self.OnModelSaveAs(None) + self.OnModelSaveAs() - def OnModelSaveAs(self, event): + def OnModelSaveAs(self, event=None): """Create model to file as""" filename = "" dlg = wx.FileDialog( @@ -1269,7 +1269,7 @@ def OnCloseWindow(self, event): message = _("Do you want to save changes in the model?") else: message = _( - "Do you want to store current model settings " "to model file?" + "Do you want to store current model settings to model file?" ) # ask user to save current settings @@ -1286,7 +1286,7 @@ def OnCloseWindow(self, event): ret = dlg.ShowModal() if ret == wx.ID_YES: if not self.modelFile: - self.OnWorkspaceSaveAs() + self.OnModelSaveAs() else: self.WriteModelFile(self.modelFile) elif ret == wx.ID_CANCEL: From 281db48052723006eed514232aeabfb6948406d3 Mon Sep 17 00:00:00 2001 From: Martin Landa Date: Mon, 6 Nov 2023 13:17:19 +0100 Subject: [PATCH 06/51] merge manually #2991 --- gui/wxpython/gmodeler/canvas.py | 16 ++++----- gui/wxpython/gmodeler/panels.py | 58 ++++++++++++++++++--------------- 2 files changed, 38 insertions(+), 36 deletions(-) diff --git a/gui/wxpython/gmodeler/canvas.py b/gui/wxpython/gmodeler/canvas.py index b7abafcca5e..04516d18761 100644 --- a/gui/wxpython/gmodeler/canvas.py +++ b/gui/wxpython/gmodeler/canvas.py @@ -75,21 +75,19 @@ def RemoveShapes(self, shapes): self.Refresh() - def GetNewShapePos(self): + def GetNewShapePos(self, yoffset=50): """Determine optimal position for newly added object :return: x,y """ - xNew, yNew = map(lambda x: x / 2, self.GetSize()) diagram = self.GetDiagram() + if diagram.GetShapeList(): + last = diagram.GetShapeList()[-1] + y = last.GetY() + last.GetBoundingBoxMin()[1] + else: + y = 20 - for shape in diagram.GetShapeList(): - y = shape.GetY() - yBox = shape.GetBoundingBoxMin()[1] / 2 - if yBox > 0 and y < yNew + yBox and y > yNew - yBox: - yNew += yBox * 3 - - return xNew, yNew + return (self.GetSize()[0] // 2, y + yoffset) def GetShapesSelected(self): """Get list of selected shapes""" diff --git a/gui/wxpython/gmodeler/panels.py b/gui/wxpython/gmodeler/panels.py index 97cd9258ddf..65e3e614961 100644 --- a/gui/wxpython/gmodeler/panels.py +++ b/gui/wxpython/gmodeler/panels.py @@ -24,6 +24,7 @@ import tempfile import random import six +import math import wx @@ -436,9 +437,10 @@ def _switchPage(self, notification): def GetOptData(self, dcmd, layer, params, propwin): """Process action data""" if params: # add data items - width, height = self.canvas.GetSize() - x = width / 2 - 200 + self._randomShift() - y = height / 2 + self._randomShift() + data_items = [] + x = layer.GetX() + y = layer.GetY() + for p in params["params"]: if p.get("prompt", "") not in ( "raster", @@ -487,9 +489,10 @@ def GetOptData(self, dcmd, layer, params, propwin): x=x, y=y, ) + data_items.append(data) self._addEvent(data) self.canvas.diagram.AddShape(data) - data.Show(True) + data.Show(False) if p.get("age", "old") == "old": rel = ModelRelation( @@ -525,11 +528,21 @@ def GetOptData(self, dcmd, layer, params, propwin): # valid / parameterized ? layer.SetValid(params) - self.canvas.Refresh() + # arrange data items + if data_items: + dc = wx.ClientDC(self.canvas) + p = 360 / len(data_items) + r = 200 + alpha = 270 * (math.pi / 180) + for data in data_items: + data.Move(dc, x + r * math.sin(alpha), y + r * 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): @@ -1034,8 +1047,8 @@ def OnAddAction(self, event): action = ModelAction( self.model, cmd=cmd, - x=x + self._randomShift(), - y=y + self._randomShift(), + x=x, + y=y, id=self.model.GetNextId(), label=label, comment=comment, @@ -1057,24 +1070,15 @@ def OnAddAction(self, event): # show properties dialog win = action.GetPropDialog() if not win: - cmdLength = len(action.GetLog(string=False)) - if cmdLength > 1 and action.IsValid(): - self.GetOptData( - dcmd=action.GetLog(string=False), - layer=action, - params=action.GetParams(), - propwin=None, - ) - else: - gmodule = GUI( - parent=self, - show=True, - giface=GraphicalModelerGrassInterface(self.model), - ) - gmodule.ParseCommand( - action.GetLog(string=False), - completed=(self.GetOptData, action, action.GetParams()), - ) + 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() @@ -1126,8 +1130,8 @@ def OnAddComment(self, event): x, y = self.canvas.GetNewShapePos() commentObj = ModelComment( self.model, - x=x + self._randomShift(), - y=y + self._randomShift(), + x=x, + y=y, id=self.model.GetNextId(), label=comment, ) From d0ca04282b3d71862e2a46c6865899a7074b3415 Mon Sep 17 00:00:00 2001 From: Martin Landa Date: Mon, 6 Nov 2023 13:55:30 +0100 Subject: [PATCH 07/51] fix indetation error --- gui/wxpython/gmodeler/panels.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/wxpython/gmodeler/panels.py b/gui/wxpython/gmodeler/panels.py index 65e3e614961..ff755da251f 100644 --- a/gui/wxpython/gmodeler/panels.py +++ b/gui/wxpython/gmodeler/panels.py @@ -1070,7 +1070,7 @@ def OnAddAction(self, event): # show properties dialog win = action.GetPropDialog() if not win: - gmodule = GUI( + gmodule = GUI( parent=self, show=True, giface=GraphicalModelerGrassInterface(self.model), From 0b67f5ac1c160c5f9be218c166c6e3c422b4c51a Mon Sep 17 00:00:00 2001 From: Martin Landa Date: Mon, 6 Nov 2023 14:11:04 +0100 Subject: [PATCH 08/51] log in main statusbar --- gui/wxpython/main_window/frame.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gui/wxpython/main_window/frame.py b/gui/wxpython/main_window/frame.py index 33184ef4b33..c5883dbf7ed 100644 --- a/gui/wxpython/main_window/frame.py +++ b/gui/wxpython/main_window/frame.py @@ -846,7 +846,8 @@ def OnGModeler(self, event=None, cmd=None): """Launch Graphical Modeler. See OnIClass documentation""" from gmodeler.panels import ModelerPanel - gmodeler_panel = ModelerPanel(parent=self, giface=self._giface) + gmodeler_panel = ModelerPanel(parent=self, giface=self._giface, + statusbar=self.statusbar) # add map display panel to notebook and make it current self.mapnotebook.AddPage(gmodeler_panel, _("Graphical Modeler")) From 15d54473f6487b8bbe10c18f34f6c13f629d982a Mon Sep 17 00:00:00 2001 From: Martin Landa Date: Mon, 6 Nov 2023 15:12:30 +0100 Subject: [PATCH 09/51] close page WIP --- gui/wxpython/gmodeler/panels.py | 42 +++++++++++++++++++++++++++---- gui/wxpython/main_window/frame.py | 18 ++++++++++++- 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/gui/wxpython/gmodeler/panels.py b/gui/wxpython/gmodeler/panels.py index ff755da251f..29a3cf967e6 100644 --- a/gui/wxpython/gmodeler/panels.py +++ b/gui/wxpython/gmodeler/panels.py @@ -85,6 +85,7 @@ def __init__( id=wx.ID_ANY, title=_("Graphical Modeler"), statusbar=None, + dockable=False, **kwargs, ): """Graphical modeler main panel @@ -185,6 +186,16 @@ def __init__( # TODO: self.onFocus = Signal("ModelerPanel.onFocus") + # TODO: base class all below + self.canCloseCallback = None + # Emitted when closing page by closing its window. + self.closingPage = Signal("ModelerPanel.closingPage") + # distinquishes whether map panel is dockable (Single-Window) + self._dockable = dockable + + # distinguishes whether map panel is docked or not + self._docked = True + def _layout(self): """Do layout""" sizer = wx.BoxSizer(wx.VERTICAL) @@ -1298,13 +1309,34 @@ def OnCloseWindow(self, event): return dlg.Destroy() - # TODO: StandaloneGrassInterface -> _docked ??? - if isinstance(self._giface, StandaloneGrassInterface): - self.parent.Destroy() + # TODO: base class + if self.canCloseCallback: + pgnum_dict = self.canCloseCallback() + if pgnum_dict is not None: + if pgnum_dict["layers"] > -1: + 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: - # TODO: why the page is not removed completely - self.Destroy() + self.parent.Destroy() + + # TODO: base class all below + 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 class VariablePanel(wx.Panel): def __init__(self, parent, id=wx.ID_ANY, **kwargs): diff --git a/gui/wxpython/main_window/frame.py b/gui/wxpython/main_window/frame.py index c5883dbf7ed..2b10b985d18 100644 --- a/gui/wxpython/main_window/frame.py +++ b/gui/wxpython/main_window/frame.py @@ -847,10 +847,26 @@ def OnGModeler(self, event=None, cmd=None): from gmodeler.panels import ModelerPanel gmodeler_panel = ModelerPanel(parent=self, giface=self._giface, - statusbar=self.statusbar) + statusbar=self.statusbar, dockable=True) # add map display panel to notebook and make it current self.mapnotebook.AddPage(gmodeler_panel, _("Graphical Modeler")) + self._setUpPage(gmodeler_panel) + + # TODO: base class + def _setUpPage(self, panel): + def CanClosePage(): + return { + "layers": self.notebookLayers.GetPageIndex(self.currentPage), + "mapnotebook": self.mapnotebook.GetPageIndex(panel) + } + + # set callbacks + panel.canCloseCallback = CanClosePage + panel.SetDockingCallback(self.mapnotebook.UndockMapDisplay) + + # bind various events + panel.closingPage.connect(self._closePageNoEvent) def OnPsMap(self, event=None, cmd=None): """Launch Cartographic Composer. See OnIClass documentation""" From b5e7fe4962166ebc67f638ee16ba26cff1e47f7d Mon Sep 17 00:00:00 2001 From: Martin Landa Date: Mon, 6 Nov 2023 15:29:07 +0100 Subject: [PATCH 10/51] fix page closing --- gui/wxpython/gmodeler/panels.py | 23 +++++++++++------------ gui/wxpython/main_window/frame.py | 4 ++-- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/gui/wxpython/gmodeler/panels.py b/gui/wxpython/gmodeler/panels.py index 29a3cf967e6..bafee90ac4e 100644 --- a/gui/wxpython/gmodeler/panels.py +++ b/gui/wxpython/gmodeler/panels.py @@ -1309,21 +1309,20 @@ def OnCloseWindow(self, event): return dlg.Destroy() - # TODO: base class + # TODO: baseclass if self.canCloseCallback: pgnum_dict = self.canCloseCallback() if pgnum_dict is not None: - if pgnum_dict["layers"] > -1: - 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 + 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() diff --git a/gui/wxpython/main_window/frame.py b/gui/wxpython/main_window/frame.py index 2b10b985d18..ee8de9de7c2 100644 --- a/gui/wxpython/main_window/frame.py +++ b/gui/wxpython/main_window/frame.py @@ -857,7 +857,6 @@ def OnGModeler(self, event=None, cmd=None): def _setUpPage(self, panel): def CanClosePage(): return { - "layers": self.notebookLayers.GetPageIndex(self.currentPage), "mapnotebook": self.mapnotebook.GetPageIndex(panel) } @@ -1008,7 +1007,8 @@ def _closePageNoEvent(self, pgnum_dict, is_docked): 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, From 14101cdd6d6987db61e5e2bbdb151ff2f2fe9298 Mon Sep 17 00:00:00 2001 From: Martin Landa Date: Tue, 7 Nov 2023 10:00:29 +0100 Subject: [PATCH 11/51] base class wip --- gui/wxpython/gmodeler/panels.py | 43 +++------------------- gui/wxpython/lmgr/frame.py | 4 +- gui/wxpython/main_window/frame.py | 55 ++++++++++------------------ gui/wxpython/main_window/notebook.py | 10 ++--- gui/wxpython/mapdisp/frame.py | 42 ++++----------------- 5 files changed, 38 insertions(+), 116 deletions(-) diff --git a/gui/wxpython/gmodeler/panels.py b/gui/wxpython/gmodeler/panels.py index bafee90ac4e..da017c0eb5b 100644 --- a/gui/wxpython/gmodeler/panels.py +++ b/gui/wxpython/gmodeler/panels.py @@ -63,6 +63,7 @@ TextCtrl, IsDark, ) +from main_window.page import MainPageBase from gmodeler.giface import GraphicalModelerGrassInterface from gmodeler.model import * from gmodeler.dialogs import * @@ -77,7 +78,7 @@ from grass.pydispatch.signal import Signal -class ModelerPanel(wx.Panel): +class ModelerPanel(wx.Panel, MainPageBase): def __init__( self, parent, @@ -113,6 +114,8 @@ def __init__( } wx.Panel.__init__(self, parent=parent, id=id, **kwargs) + MainPageBase.__init__(self, dockable) + self.SetName("Modeler") self.toolbar = ModelerToolbar(parent=self) @@ -186,16 +189,6 @@ def __init__( # TODO: self.onFocus = Signal("ModelerPanel.onFocus") - # TODO: base class all below - self.canCloseCallback = None - # Emitted when closing page by closing its window. - self.closingPage = Signal("ModelerPanel.closingPage") - # distinquishes whether map panel is dockable (Single-Window) - self._dockable = dockable - - # distinguishes whether map panel is docked or not - self._docked = True - def _layout(self): """Do layout""" sizer = wx.BoxSizer(wx.VERTICAL) @@ -1309,33 +1302,7 @@ def OnCloseWindow(self, event): return dlg.Destroy() - # TODO: baseclass - 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() - - # TODO: base class all below - 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 + self._onCloseWindow(event) class VariablePanel(wx.Panel): def __init__(self, parent, id=wx.ID_ANY, **kwargs): diff --git a/gui/wxpython/lmgr/frame.py b/gui/wxpython/lmgr/frame.py index 5e940c67dc3..7a90be4b1d6 100644 --- a/gui/wxpython/lmgr/frame.py +++ b/gui/wxpython/lmgr/frame.py @@ -574,7 +574,7 @@ def CanCloseDisplay(askIfSaveWorkspace): return pgnum_dict return None - mapdisplay.canCloseDisplayCallback = CanCloseDisplay + mapdisplay.canCloseCallback = CanCloseDisplay # bind various events mapdisplay.BindToFrame( @@ -589,7 +589,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( diff --git a/gui/wxpython/main_window/frame.py b/gui/wxpython/main_window/frame.py index ee8de9de7c2..59da1fb7d86 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 @@ -317,10 +317,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""" @@ -434,7 +434,7 @@ def CreateNewMapDisplay(giface, layertree): # create Map Display mapdisplay = MapPanel( - parent=self.mapnotebook, + parent=self.mainnotebook, giface=giface, id=wx.ID_ANY, tree=layertree, @@ -445,7 +445,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) @@ -496,13 +496,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 ( @@ -511,9 +511,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( @@ -527,7 +525,6 @@ def CanCloseDisplay(askIfSaveWorkspace): ) mapdisplay.starting3dMode.connect(self.AddNvizTools) mapdisplay.ending3dMode.connect(self.RemoveNvizTools) - mapdisplay.closingDisplay.connect(self._closePageNoEvent) # set default properties mapdisplay.SetProperties( @@ -553,7 +550,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) @@ -600,7 +597,7 @@ def BuildPanes(self): ) self._auimgr.AddPane( - self.mapnotebook, + self.mainnotebook, aui.AuiPaneInfo().Name("map display content").CenterPane().PaneBorder(True), ) @@ -848,24 +845,10 @@ def OnGModeler(self, event=None, cmd=None): gmodeler_panel = ModelerPanel(parent=self, giface=self._giface, statusbar=self.statusbar, dockable=True) + gmodeler_panel.SetUpPage(self, self.mainnotebook) # add map display panel to notebook and make it current - self.mapnotebook.AddPage(gmodeler_panel, _("Graphical Modeler")) - self._setUpPage(gmodeler_panel) - - # TODO: base class - def _setUpPage(self, panel): - def CanClosePage(): - return { - "mapnotebook": self.mapnotebook.GetPageIndex(panel) - } - - # set callbacks - panel.canCloseCallback = CanClosePage - panel.SetDockingCallback(self.mapnotebook.UndockMapDisplay) - - # bind various events - panel.closingPage.connect(self._closePageNoEvent) + self.mainnotebook.AddPage(gmodeler_panel, _("Graphical Modeler")) def OnPsMap(self, event=None, cmd=None): """Launch Cartographic Composer. See OnIClass documentation""" @@ -967,8 +950,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.SetSelectionToMapPage(self.GetMapDisplay()) event.Skip() @@ -986,7 +969,7 @@ def OnCBPageClosing(self, event): maptree = self.notebookLayers.GetPage(event.GetSelection()).maptree maptree.GetMapDisplay().CleanUp() - self.mapnotebook.DeleteMapPage(self.GetMapDisplay()) + self.mainnotebook.DeleteMapPage(self.GetMapDisplay()) maptree.Close(True) self.currentPage = None @@ -1000,7 +983,7 @@ 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 @@ -1014,7 +997,7 @@ def _closePageNoEvent(self, pgnum_dict, is_docked): 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.""" @@ -1152,7 +1135,7 @@ def GetAuiNotebook(self): :return: aui notebook instance """ - return self.mapnotebook + return self.mainnotebook def GetLayerNotebook(self): """Get Layers Notebook""" @@ -1695,7 +1678,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.SetMapPageText(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 9cc313a80f4..9c80aa22c68 100644 --- a/gui/wxpython/main_window/notebook.py +++ b/gui/wxpython/main_window/notebook.py @@ -5,7 +5,7 @@ Classes: - notebook::MapPageFrame - - notebook::MapNotebook + - notebook::MainNotebook (C) 2022 by the GRASS Development Team @@ -67,7 +67,7 @@ def OnClose(self, event): self.mapdisplay.OnCloseWindow(event=None, askIfSaveWorkspace=True) -class MapNotebook(aui.AuiNotebook): +class MainNotebook(aui.AuiNotebook): """Map notebook class. Overrides some AuiNotebook classes. Takes into consideration the dock/undock functionality. """ @@ -124,7 +124,7 @@ def AddPage(self, *args, **kwargs): self.currentDisplay = self.GetCurrentPage() def SetSelectionToMapPage(self, page): - """Decides whether to set selection to a MapNotebook page + """Decides whether to set selection to a MainNotebook page or an undocked independent frame""" self.SetSelection(self.GetPageIndex(page)) @@ -133,7 +133,7 @@ def SetSelectionToMapPage(self, page): wx.CallLater(500, lambda: frame.Raise() if frame else None) def DeleteMapPage(self, page): - """Decides whether to delete a MapNotebook page + """Decides whether to delete a MainNotebook page or close an undocked independent frame""" if page.IsDocked(): self.DeletePage(self.GetPageIndex(page)) @@ -142,7 +142,7 @@ def DeleteMapPage(self, page): frame.Destroy() def SetMapPageText(self, page, text): - """Decides whether sets title to MapNotebook page + """Decides whether sets title to MainNotebook page or an undocked independent frame""" if page.IsDocked(): self.SetPageText(page_idx=self.GetPageIndex(page), text=text) diff --git a/gui/wxpython/mapdisp/frame.py b/gui/wxpython/mapdisp/frame.py index bcb59f74994..53175bb3b53 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,17 +113,6 @@ 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 # Emitted when switching map notebook tabs (Single-Window) self.onFocus = Signal("MapPanel.onFocus") @@ -133,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") @@ -674,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() @@ -998,22 +970,22 @@ def OnCloseWindow(self, event, askIfSaveWorkspace=True): Also close associated layer tree page """ Debug.msg(2, "MapPanel.OnCloseWindow()") - if self.canCloseDisplayCallback: - pgnum_dict = self.canCloseDisplayCallback( + 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() From a710fb31149ded75956d7fb47aeff072c6a62ad2 Mon Sep 17 00:00:00 2001 From: Martin Landa Date: Tue, 7 Nov 2023 14:50:43 +0100 Subject: [PATCH 12/51] remove currentDisplay --- gui/wxpython/main_window/notebook.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/gui/wxpython/main_window/notebook.py b/gui/wxpython/main_window/notebook.py index 9c80aa22c68..46c4b04f23b 100644 --- a/gui/wxpython/main_window/notebook.py +++ b/gui/wxpython/main_window/notebook.py @@ -82,8 +82,6 @@ def __init__( self.SetArtProvider(SimpleTabArt()) - self.currentDisplay = None - # bindings self.Bind( aui.EVT_AUINOTEBOOK_PAGE_CHANGED, @@ -120,8 +118,6 @@ def AddPage(self, *args, **kwargs): Adds page to notebook and makes it current""" super().AddPage(*args, **kwargs) self.SetSelection(self.GetPageCount() - 1) - if isinstance(args[0], MapPanel): - self.currentDisplay = self.GetCurrentPage() def SetSelectionToMapPage(self, page): """Decides whether to set selection to a MainNotebook page @@ -154,11 +150,8 @@ def SetMapPageText(self, page, text): def OnClose(self, event): """Page of map notebook is being closed""" page = self.GetCurrentPage() - if page == self.currentDisplay: + if isinstance(page, MapPanel): page.OnCloseWindow(event=None, askIfSaveWorkspace=True) else: page.OnCloseWindow(event=None) event.Veto() - - def GetCurrentDisplay(self): - return self.currentDisplay From de667883e1a260c1759e52e8d0be3a5958499009 Mon Sep 17 00:00:00 2001 From: Martin Landa Date: Tue, 7 Nov 2023 15:53:59 +0100 Subject: [PATCH 13/51] WIP --- gui/wxpython/gmodeler/panels.py | 37 ++++++++++--------------------- gui/wxpython/main_window/frame.py | 5 +++++ gui/wxpython/mapdisp/frame.py | 3 --- 3 files changed, 17 insertions(+), 28 deletions(-) diff --git a/gui/wxpython/gmodeler/panels.py b/gui/wxpython/gmodeler/panels.py index da017c0eb5b..db89e4f7200 100644 --- a/gui/wxpython/gmodeler/panels.py +++ b/gui/wxpython/gmodeler/panels.py @@ -72,7 +72,7 @@ from gmodeler.preferences import PreferencesDialog, PropertiesDialog wxModelDone, EVT_MODEL_DONE = NewEvent() - +0 from grass.script.utils import try_remove from grass.script import core as grass from grass.pydispatch.signal import Signal @@ -186,9 +186,6 @@ def __init__( if self.goutput: self.goutput.SetSashPosition(int(self.GetSize()[1] * 0.75)) - # TODO: - self.onFocus = Signal("ModelerPanel.onFocus") - def _layout(self): """Do layout""" sizer = wx.BoxSizer(wx.VERTICAL) @@ -213,19 +210,8 @@ def _randomShift(self): """Returns random value to shift layout""" return random.randint(-self.randomness, self.randomness) - # TODO: - def SetTitle(self, title): - if isinstance(self._giface, StandaloneGrassInterface): - self.parent.SetTitle(title) - - # TODO: def SetStatusText(self, *args): - if isinstance(self._giface, StandaloneGrassInterface): - self.GetParent().SetStatusText(*args) - else: - # TODO: how to? - # self.SetStatusText(*args) - pass + self.statusbar.SetStatusText(*args) def GetStatusBar(self): """Get statusbar""" @@ -245,13 +231,13 @@ def ModelChanged(self, changed=True): if self.modelFile: if self.modelChanged: - self.SetTitle( + self.RenamePage( self.baseTitle + " - " + os.path.basename(self.modelFile) + "*" ) else: - self.SetTitle(self.baseTitle + " - " + os.path.basename(self.modelFile)) + self.RenamePage(self.baseTitle + " - " + os.path.basename(self.modelFile)) else: - self.SetTitle(self.baseTitle) + self.RenamePage(self.baseTitle) def OnPageChanged(self, event): """Page in notebook changed""" @@ -593,7 +579,8 @@ def LoadModelFile(self, filename): return self.modelFile = filename - self.SetTitle(self.baseTitle + " - " + os.path.basename(self.modelFile)) + + self.RenamePage(self.baseTitle + " - " + os.path.basename(self.modelFile)) self.SetStatusText(_("Please wait, loading model..."), 0) @@ -810,7 +797,7 @@ def OnModelNew(self, event): # no model file loaded self.modelFile = None self.modelChanged = False - self.SetTitle(self.baseTitle) + self.RenamePage(self.baseTitle) def OnModelOpen(self, event): """Load model from file""" @@ -835,7 +822,7 @@ def OnModelOpen(self, event): self.LoadModelFile(filename) self.modelFile = filename - self.SetTitle(self.baseTitle + " - " + os.path.basename(self.modelFile)) + self.RenamePage(self.baseTitle + " - " + os.path.basename(self.modelFile)) self.SetStatusText( _("%(items)d items (%(actions)d actions) loaded into model") % { @@ -864,7 +851,7 @@ def OnModelSave(self, event=None): 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)) + self.RenamePage(self.baseTitle + " - " + os.path.basename(self.modelFile)) elif not self.modelFile: self.OnModelSaveAs() @@ -908,7 +895,7 @@ def OnModelSaveAs(self, event=None): self.WriteModelFile(filename) self.modelFile = filename - self.SetTitle(self.baseTitle + " - " + os.path.basename(self.modelFile)) + self.RenamePage(self.baseTitle + " - " + os.path.basename(self.modelFile)) self.SetStatusText(_("File <%s> saved") % self.modelFile, 0) def OnModelClose(self, event=None): @@ -940,7 +927,7 @@ def OnModelClose(self, event=None): dlg.Destroy() self.modelFile = None - self.SetTitle(self.baseTitle) + self.RenamePage(self.baseTitle) self.canvas.GetDiagram().DeleteAllShapes() self.model.Reset() diff --git a/gui/wxpython/main_window/frame.py b/gui/wxpython/main_window/frame.py index 59da1fb7d86..0aff063b14e 100644 --- a/gui/wxpython/main_window/frame.py +++ b/gui/wxpython/main_window/frame.py @@ -976,6 +976,11 @@ def OnCBPageClosing(self, event): event.Skip() + def _renamePageNoEvent(self, pgnum_dict, is_docked, text): + if is_docked: + self.mainnotebook.SetMapPageText( + 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 diff --git a/gui/wxpython/mapdisp/frame.py b/gui/wxpython/mapdisp/frame.py index 53175bb3b53..05874a9d842 100644 --- a/gui/wxpython/mapdisp/frame.py +++ b/gui/wxpython/mapdisp/frame.py @@ -114,9 +114,6 @@ def __init__( # used for VDigit toolbar and window and GLWindow self.tree = tree - # 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") From 9438c1ca19b0af031bba1d677fb58fb7fb3921cd Mon Sep 17 00:00:00 2001 From: Martin Landa Date: Tue, 7 Nov 2023 17:39:32 +0100 Subject: [PATCH 14/51] undock/dock --- gui/wxpython/gmodeler/toolbars.py | 31 ++++++++++++++------- gui/wxpython/main_window/notebook.py | 40 +++++++++++++++------------- 2 files changed, 43 insertions(+), 28 deletions(-) diff --git a/gui/wxpython/gmodeler/toolbars.py b/gui/wxpython/gmodeler/toolbars.py index cee01d094db..a8dd90755c5 100644 --- a/gui/wxpython/gmodeler/toolbars.py +++ b/gui/wxpython/gmodeler/toolbars.py @@ -16,6 +16,8 @@ import sys +import wx + from gui_core.toolbars import BaseToolbar, BaseIcons from icons.icon import MetaIcon @@ -70,15 +72,14 @@ def _toolbarData(self): "quit": BaseIcons["quit"], } - return self._getToolbarData( + data = ( ( - ( ("new", icons["new"].label.rsplit(" ", 1)[0]), - icons["new"], - self.parent.OnModelNew, + icons["new"], + self.parent.OnModelNew, ), - ( - ("open", icons["open"].label.rsplit(" ", 1)[0]), + ( + ("open", icons["open"].label.rsplit(" ", 1)[0]), icons["open"], self.parent.OnModelOpen, ), @@ -156,10 +157,22 @@ def _toolbarData(self): self.parent.OnHelp, ), (None,), + ) + if self.parent.IsDockable(): + data += ( ( - ("quit", icons["quit"].label), - icons["quit"], - self.parent.OnCloseWindow, + ("mapDispDocking", BaseIcons["mapDispDocking"].label), + BaseIcons["mapDispDocking"], + 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/main_window/notebook.py b/gui/wxpython/main_window/notebook.py index 46c4b04f23b..1092d78b01e 100644 --- a/gui/wxpython/main_window/notebook.py +++ b/gui/wxpython/main_window/notebook.py @@ -25,46 +25,46 @@ from gui_core.wrap import SimpleTabArt from mapdisp.frame import MapPanel -class MapPageFrame(wx.Frame): +class MainPageFrame(wx.Frame): """Frame for independent map display window.""" - def __init__(self, parent, mapdisplay, size, pos, title): + def __init__(self, parent, panel, size, pos, title, icon="grass"): 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 + os.path.join(globalvar.ICONDIR, icon + ".ico"), wx.BITMAP_TYPE_ICO ) ) - self.mapdisplay.onFocus.emit() + 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) + self.panel.OnCloseWindow(event=None, askIfSaveWorkspace=True) # TODO class MainNotebook(aui.AuiNotebook): @@ -89,27 +89,29 @@ def __init__( ) self.Bind(aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self.OnClose) - def UndockMapDisplay(self, page): - """Undock active map display to independent MapFrame object""" + def UndockPage(self, page): + """Undock active page to independent MapFrame 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" self.RemovePage(index) - frame = MapPageFrame( + frame = MainPageFrame( parent=self.parent, - mapdisplay=page, + panel=page, size=original_size, pos=original_pos, title=text, + icon=icon ) - frame.SetDockingCallback(self.DockMapDisplay) + frame.SetDockingCallback(self.DockPage) - def DockMapDisplay(self, page): + def DockPage(self, page): """Dock independent MapFrame object back to Aui.Notebook""" frame = page.GetParent() page.Reparent(self) - page.SetDockingCallback(self.UndockMapDisplay) + page.SetDockingCallback(self.UndockPage) self.AddPage(page, frame.GetTitle()) frame.Destroy() From 1ea3826ee654b132a4842b624d52088b589319b9 Mon Sep 17 00:00:00 2001 From: Martin Landa Date: Tue, 7 Nov 2023 18:11:39 +0100 Subject: [PATCH 15/51] rename methods map -> main --- gui/wxpython/main_window/frame.py | 8 ++++---- gui/wxpython/main_window/notebook.py | 20 +++++++++++--------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/gui/wxpython/main_window/frame.py b/gui/wxpython/main_window/frame.py index 0aff063b14e..256fdc2a6e6 100644 --- a/gui/wxpython/main_window/frame.py +++ b/gui/wxpython/main_window/frame.py @@ -951,7 +951,7 @@ def OnCBPageChanged(self, event): self.currentPageNum = self.notebookLayers.GetSelection() if hasattr(self.currentPage, "maptree") and self.mainnotebook.GetCurrentPage(): - self.mainnotebook.SetSelectionToMapPage(self.GetMapDisplay()) + self.mainnotebook.SetSelectionToMainPage(self.GetMapDisplay()) event.Skip() @@ -969,7 +969,7 @@ def OnCBPageClosing(self, event): maptree = self.notebookLayers.GetPage(event.GetSelection()).maptree maptree.GetMapDisplay().CleanUp() - self.mainnotebook.DeleteMapPage(self.GetMapDisplay()) + self.mainnotebook.DeleteMainPage(self.GetMapDisplay()) maptree.Close(True) self.currentPage = None @@ -978,7 +978,7 @@ def OnCBPageClosing(self, event): def _renamePageNoEvent(self, pgnum_dict, is_docked, text): if is_docked: - self.mainnotebook.SetMapPageText( + self.mainnotebook.SetMainPageText( self.mainnotebook.GetPage(pgnum_dict["mainnotebook"]), text) def _closePageNoEvent(self, pgnum_dict, is_docked): @@ -1683,7 +1683,7 @@ def OnRenameDisplay(self, event): if dlg.ShowModal() == wx.ID_OK: name = dlg.GetValue() self.notebookLayers.SetPageText(page=self.currentPageNum, text=name) - self.mainnotebook.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 1092d78b01e..6cf496b6779 100644 --- a/gui/wxpython/main_window/notebook.py +++ b/gui/wxpython/main_window/notebook.py @@ -4,7 +4,7 @@ @brief Custom AuiNotebook class and class for undocked AuiNotebook frame Classes: - - notebook::MapPageFrame + - notebook::MainPageFrame - notebook::MainNotebook (C) 2022 by the GRASS Development Team @@ -64,11 +64,13 @@ def SetDockingCallback(self, function): def OnClose(self, event): """Close frame and associated layer notebook page.""" - self.panel.OnCloseWindow(event=None, askIfSaveWorkspace=True) # TODO - + if isinstance(self.panel, MapPanel): + self.panel.OnCloseWindow(event=None, askIfSaveWorkspace=True) + else: + self.panel.OnCloseWindow(event=None) class MainNotebook(aui.AuiNotebook): - """Map notebook class. Overrides some AuiNotebook classes. + """Main notebook class. Overrides some AuiNotebook classes. Takes into consideration the dock/undock functionality. """ @@ -90,7 +92,7 @@ def __init__( self.Bind(aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self.OnClose) def UndockPage(self, page): - """Undock active page to independent MapFrame object""" + """Undock active page to independent MainFrame object""" index = self.GetPageIndex(page) text = self.GetPageText(index) original_size = page.GetSize() @@ -108,7 +110,7 @@ def UndockPage(self, page): frame.SetDockingCallback(self.DockPage) def DockPage(self, page): - """Dock independent MapFrame object back to Aui.Notebook""" + """Dock independent MainFrame object back to Aui.Notebook""" frame = page.GetParent() page.Reparent(self) page.SetDockingCallback(self.UndockPage) @@ -121,7 +123,7 @@ def AddPage(self, *args, **kwargs): super().AddPage(*args, **kwargs) self.SetSelection(self.GetPageCount() - 1) - def SetSelectionToMapPage(self, page): + def SetSelectionToMainPage(self, page): """Decides whether to set selection to a MainNotebook page or an undocked independent frame""" self.SetSelection(self.GetPageIndex(page)) @@ -130,7 +132,7 @@ def SetSelectionToMapPage(self, page): frame = page.GetParent() wx.CallLater(500, lambda: frame.Raise() if frame else None) - def DeleteMapPage(self, page): + def DeleteMainPage(self, page): """Decides whether to delete a MainNotebook page or close an undocked independent frame""" if page.IsDocked(): @@ -139,7 +141,7 @@ def DeleteMapPage(self, page): frame = page.GetParent() frame.Destroy() - def SetMapPageText(self, page, text): + def SetMainPageText(self, page, text): """Decides whether sets title to MainNotebook page or an undocked independent frame""" if page.IsDocked(): From 4e45154af210de82b74eb98424d3fb4167a94b48 Mon Sep 17 00:00:00 2001 From: Martin Landa Date: Wed, 8 Nov 2023 11:20:10 +0100 Subject: [PATCH 16/51] add missing module --- gui/wxpython/main_window/page.py | 102 +++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 gui/wxpython/main_window/page.py diff --git a/gui/wxpython/main_window/page.py b/gui/wxpython/main_window/page.py new file mode 100644 index 00000000000..ef820e0172b --- /dev/null +++ b/gui/wxpython/main_window/page.py @@ -0,0 +1,102 @@ +""" +@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 + +class MainPageBase: + def __init__(self, dockable): + + 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 SetUpPage(self, parent, notebook, can_close=None): + def CanClosePage(): + return { + "mainnotebook": notebook.GetPageIndex(self) + } + + # 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) + + 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): + if self.canCloseCallback: + pgnum_dict = self.canCloseCallback() # TODO + 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) From 1d8ee1f0af5ec937f441e2ee2cff6e60f96c1a09 Mon Sep 17 00:00:00 2001 From: Martin Landa Date: Wed, 8 Nov 2023 11:36:37 +0100 Subject: [PATCH 17/51] +_pgnumDict() --- gui/wxpython/main_window/page.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/gui/wxpython/main_window/page.py b/gui/wxpython/main_window/page.py index ef820e0172b..984f3338302 100644 --- a/gui/wxpython/main_window/page.py +++ b/gui/wxpython/main_window/page.py @@ -19,6 +19,7 @@ class MainPageBase: def __init__(self, dockable): + self.notebook = None self.canCloseCallback = None @@ -39,12 +40,18 @@ def __init__(self, dockable): # Emitted when renaming page. self.renamingPage = Signal("MainPage.renamingPage") - + + def _pgnumDict(self): + """Get dictionary containg page index""" + return { + "mainnotebook": self.notebook.GetPageIndex(self) + } + def SetUpPage(self, parent, notebook, can_close=None): + self.notebook = notebook + def CanClosePage(): - return { - "mainnotebook": notebook.GetPageIndex(self) - } + return self._pgnumDict() # set callbacks self.canCloseCallback = CanClosePage if can_close is None else can_close @@ -89,8 +96,9 @@ def _onCloseWindow(self, event): self.parent.Destroy() def RenamePage(self, title): + """Rename page or change frame title""" if self.canCloseCallback: - pgnum_dict = self.canCloseCallback() # TODO + pgnum_dict = self._pgnumDict() if pgnum_dict is not None: if self.IsDockable(): self.renamingPage.emit( From abcdc9b8c15980441fea6427398678702859af75 Mon Sep 17 00:00:00 2001 From: Martin Landa Date: Wed, 8 Nov 2023 11:41:02 +0100 Subject: [PATCH 18/51] rename notebook to _mainnotebook to avoid variable override --- gui/wxpython/main_window/page.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gui/wxpython/main_window/page.py b/gui/wxpython/main_window/page.py index 984f3338302..d8e5aa5ef68 100644 --- a/gui/wxpython/main_window/page.py +++ b/gui/wxpython/main_window/page.py @@ -19,7 +19,7 @@ class MainPageBase: def __init__(self, dockable): - self.notebook = None + self._mainnotebook = None self.canCloseCallback = None @@ -44,11 +44,11 @@ def __init__(self, dockable): def _pgnumDict(self): """Get dictionary containg page index""" return { - "mainnotebook": self.notebook.GetPageIndex(self) + "mainnotebook": self._mainnotebook.GetPageIndex(self) } def SetUpPage(self, parent, notebook, can_close=None): - self.notebook = notebook + self._mainnotebook = notebook def CanClosePage(): return self._pgnumDict() From b591ca786d47cb24998bd661172d30373e192e63 Mon Sep 17 00:00:00 2001 From: Martin Landa Date: Wed, 8 Nov 2023 12:01:16 +0100 Subject: [PATCH 19/51] new icon modeler-settings, add model properies into toolbar' --- gui/icons/grass/modeler-settings.png | Bin 0 -> 5376 bytes gui/wxpython/gmodeler/toolbars.py | 12 +++++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 gui/icons/grass/modeler-settings.png diff --git a/gui/icons/grass/modeler-settings.png b/gui/icons/grass/modeler-settings.png new file mode 100644 index 0000000000000000000000000000000000000000..11a07609cbe1bcfb0fb603784c7ed78e30df32e2 GIT binary patch literal 5376 zcmeHKc~nzZ6Az-YsihSaaSK8D1kAoi!XiNk5{!T>D$3*KC3!*?l0YIZ1ke@)7X-0o zQ9(rnMMV&SBA^JZmWqN4DAuJG6;u=zaKXL=R6JkL`Hts&{mP&mrpp09{1*=UG(Tg!;SZv%XUU5%!tj6D?B1J=>!Cx;x`+jW1{31JNzlvvJ+ej z{iPXVd1(T;J)!!?Wk2^lw79y;ctUYJy5HdL zqr3YbVCD?s?Mf4$-99zz=@a&oTLue?&Qng{4aAj@z?VzhwQ7>#QA-zd>w5J0-wt2o z`daP2*EQ4W^sRGiGWsf<8}}|FOuaoZY2smNQ2n%ea#lc2uT$j4ozTwyY=*FTOwz21 z;!|%MD*ilHOSV}^BQKtAJ$VuP@C$;&G^=NnNdGIU^HUVKqU>@;_?59;H`8heE1Cl{ zDtwgV?B<1;e8-t9-WzW}uO7a_C8p-S+y6ss>$63F`u?)iz@~B3>Spx(4L#2N9Y=z* zGgWD4xO?B9JL%jISBqG6>h+ggm!i$V^_v5Exsi&BM}g6Nr0g^>QeHtkZx$$%-~fRv48mz7a-{B1C`V_F900>16*>$O!BRHn zX;lpd4GY+qKq`;Glef;+6QBOAjuO|ItE6fe4n!^;QVkpJ}RK z;U}^_i%mD9PiJHxi1|m{&$K_}u2)91csv$I21e<^<8s&-oqv`<2EqcC{uQJP2?9FE z#0qJ2I+j8Z(69gj48sa3L?Rs!5Gj-}<_IXRRH*`_Af$srz;Q5wBczaoU>HEaQW+o_ zOQC@bEQ1M5{~dm!jAw{Ld1ZL z@rI>Qnokq{umti`0XjB`G$xZmA<$_wDwRYg4yCQf3xE_#q!M+QL;{XHl#Om#SV%Gm zv4E~l5db|bk_*dC0RbwR!e1tfU}JO;Xr1N9X&$nj1b_ToK%>7p`67P5()E?D zFJj<}l)qNjSGvB4fiF`2T3!D)x=cSErywcv6{JQEOFjIDE0KfN*sw(&9MpT=e{c1{ zG(=(|_Y77dr?YFi%V5FWEDa(wQgM0iMxAEHrWD+)%zK+rsL_R7j;p_C&(g{(5As=wM~{SD3=T<#Q594Qr>gs#N54}0p|42a;3$VGZU;-HJts_-BV|t zyB{4ac}NR7JeluBWH1ur1`SjmmHT4KeVCoR)%nZTKge-=mo}=vkw5S_{gB_Sx%2Ex z&zEE~_NJeq@c3Sr+Ai-q`M!9$I3>*_X1sY^d#`irEN>tC?B1w~H5G@Nrj9v4iF{ny zS|mMEY>nF$e)CCX@eTFk_Uef)_S)IpE%c)u-rltg!+qn@mk=6i83{+x9dFk?6$R8a z#ICRT;~N*op5GicyS25qblBv~kCJW?ReD^?Umb8IZ^4e)KjtUh$pr)#1wvpf_4NxU z`;Fof;W9r)(2{^T4foN(?xuk>&yv!~+AL+U0phJvYiwrg>tCAp^CLIo<2lBPAoJGO z?>4lywxZB4wLwPl)lD9cYWNFLcQ@srUU_@Pj=HfqdDZTzrkhqL)P-Um_ultkQ@t{q zy;eA1yF@EGm%lzs8rd*$p7Xfz%iVI#?H%l!&5i4`-h9{R(|4?DoooP03f7WhgWo@T zS>$14Ui;HTSBEZT;m%2u&^t!gF|9@!)VCT52a{ht*y3<4byUyghQe*5t>-uT8lpT+ z>n;c8dzkI2YWk0>mzS6PZGYsk(!}7P?i7;OrAzIprpNLQpTB+_{2`>Jlz1?agXw>s z^==&)b?3I{*2p;L4b`HGwyXPMdPxt=E<_L~@9KYfVTNXhVO2gHNp+l2xO8%IAdj4; zEa4pME}MEhZ|Reu-}e|zf2f{WxbX)A)J0Kv*oqxl+peZ1F)WkvYBofd{=TkbJ2bWM zg|XSLKhLC=jYi4uzlBNgByNcXbKu^u=^;zV&JVUwlY?EhqdjRY zcW!l86cpXF5gplbVN=KCy6kv&ORMR%jbcT8-V*I^Q=^D+eA{r_NSg<6anaiH);4Br zHNA5p?ZqS^-fpaMT+@@6IymBT~$IG;v9Vp@hWmT~G-3J%Wo_;S(#`2F46r zJvs1n+V`n(^RHF;KC(Q}-?zcoaD6ox=@JtaqIOzYeF(68H`mTC4*xcTrFll*X>LM9 z{c@h`*wTC3Nm>>%Dp0Y}`$kg7Pt1(8<3?IGbI;FU;gr}dOtTeo0bA0L(IUFEbEVbe zl2c7R9d9ntAA6Lgo7}CbO5kd%l)^WwJu=_-_AH#DtpL|wzTRUlRvAy(x8s>-4R4b< z&vE<-+u6b9$A9g5nP+Y8La=;0n0_PYn#?jZB|g)(0Jil!xxX!xrw)a>&n)%((ZRWS z>pJ@je|bHRm=!wB#VV^4Ud|6}#7{qd6H0NKvg**;%nNDMqhdP)?djc8RfyD}^QJ|1 dkzYQqF*d9npFe=jpN2Ft6xZE{Q@kKF{%@?KFJ=G$ literal 0 HcmV?d00001 diff --git a/gui/wxpython/gmodeler/toolbars.py b/gui/wxpython/gmodeler/toolbars.py index a8dd90755c5..dc8af1b0963 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. @@ -62,8 +62,8 @@ def _toolbarData(self): "comment": MetaIcon(img="label-add", label=_("Add comment to model")), "run": MetaIcon(img="execute", label=_("Run model")), "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") ), @@ -146,6 +146,12 @@ def _toolbarData(self): icons["variables"], self.parent.OnVariables, ), + ( + ("properties", icons["properties"].label), + icons["properties"], + self.parent.OnModelProperties, + ), + (None,), ( ("settings", icons["settings"].label), icons["settings"], From dcad156cbf1f7877c87171c414f2e9b0e3ea7e3c Mon Sep 17 00:00:00 2001 From: Martin Landa Date: Wed, 8 Nov 2023 12:10:20 +0100 Subject: [PATCH 20/51] black applied --- gui/wxpython/gmodeler/canvas.py | 1 + gui/wxpython/gmodeler/frame.py | 5 +- gui/wxpython/gmodeler/panels.py | 9 +- gui/wxpython/gmodeler/toolbars.py | 170 +++++++++++++-------------- gui/wxpython/main_window/frame.py | 8 +- gui/wxpython/main_window/notebook.py | 8 +- gui/wxpython/main_window/page.py | 11 +- gui/wxpython/mapdisp/frame.py | 4 +- 8 files changed, 111 insertions(+), 105 deletions(-) diff --git a/gui/wxpython/gmodeler/canvas.py b/gui/wxpython/gmodeler/canvas.py index 04516d18761..55947bee1ab 100644 --- a/gui/wxpython/gmodeler/canvas.py +++ b/gui/wxpython/gmodeler/canvas.py @@ -22,6 +22,7 @@ from gmodeler.model import * from gmodeler.dialogs import * + class ModelCanvas(ogl.ShapeCanvas): """Canvas where model is drawn""" diff --git a/gui/wxpython/gmodeler/frame.py b/gui/wxpython/gmodeler/frame.py index 56fa72ca3eb..d22f33dd3a3 100644 --- a/gui/wxpython/gmodeler/frame.py +++ b/gui/wxpython/gmodeler/frame.py @@ -26,6 +26,7 @@ from gmodeler.menudata import ModelerMenuData from gmodeler.panels import ModelerPanel + class ModelerFrame(wx.Frame): def __init__( self, parent, giface, id=wx.ID_ANY, title=_("Graphical Modeler"), **kwargs @@ -46,7 +47,7 @@ def __init__( self.statusbar = self.CreateStatusBar(number=1) self.panel = ModelerPanel(parent=self, giface=giface, statusbar=self.statusbar) - + self.menubar = Menubar( parent=self, model=ModelerMenuData().GetModel(separators=True) ) @@ -54,4 +55,4 @@ def __init__( self.SetName("ModelerFrame") self.SetMinSize((640, 300)) - self.SetSize((800, 600)) \ No newline at end of file + self.SetSize((800, 600)) diff --git a/gui/wxpython/gmodeler/panels.py b/gui/wxpython/gmodeler/panels.py index db89e4f7200..bea54d86696 100644 --- a/gui/wxpython/gmodeler/panels.py +++ b/gui/wxpython/gmodeler/panels.py @@ -235,7 +235,9 @@ def ModelChanged(self, changed=True): self.baseTitle + " - " + os.path.basename(self.modelFile) + "*" ) else: - self.RenamePage(self.baseTitle + " - " + os.path.basename(self.modelFile)) + self.RenamePage( + self.baseTitle + " - " + os.path.basename(self.modelFile) + ) else: self.RenamePage(self.baseTitle) @@ -851,7 +853,9 @@ def OnModelSave(self, event=None): Debug.msg(4, "ModelFrame.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)) + self.RenamePage( + self.baseTitle + " - " + os.path.basename(self.modelFile) + ) elif not self.modelFile: self.OnModelSaveAs() @@ -1291,6 +1295,7 @@ def OnCloseWindow(self, event): self._onCloseWindow(event) + class VariablePanel(wx.Panel): def __init__(self, parent, id=wx.ID_ANY, **kwargs): """Manage model variables panel""" diff --git a/gui/wxpython/gmodeler/toolbars.py b/gui/wxpython/gmodeler/toolbars.py index dc8af1b0963..8bf99161f88 100644 --- a/gui/wxpython/gmodeler/toolbars.py +++ b/gui/wxpython/gmodeler/toolbars.py @@ -74,95 +74,95 @@ def _toolbarData(self): data = ( ( - ("new", icons["new"].label.rsplit(" ", 1)[0]), + ("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,), + 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 += ( diff --git a/gui/wxpython/main_window/frame.py b/gui/wxpython/main_window/frame.py index 256fdc2a6e6..089fda5db05 100644 --- a/gui/wxpython/main_window/frame.py +++ b/gui/wxpython/main_window/frame.py @@ -843,8 +843,9 @@ def OnGModeler(self, event=None, cmd=None): """Launch Graphical Modeler. See OnIClass documentation""" from gmodeler.panels import ModelerPanel - gmodeler_panel = ModelerPanel(parent=self, giface=self._giface, - statusbar=self.statusbar, dockable=True) + gmodeler_panel = ModelerPanel( + parent=self, giface=self._giface, statusbar=self.statusbar, dockable=True + ) gmodeler_panel.SetUpPage(self, self.mainnotebook) # add map display panel to notebook and make it current @@ -979,7 +980,8 @@ def OnCBPageClosing(self, event): def _renamePageNoEvent(self, pgnum_dict, is_docked, text): if is_docked: self.mainnotebook.SetMainPageText( - self.mainnotebook.GetPage(pgnum_dict["mainnotebook"]), text) + 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 diff --git a/gui/wxpython/main_window/notebook.py b/gui/wxpython/main_window/notebook.py index 6cf496b6779..9b4843d28c9 100644 --- a/gui/wxpython/main_window/notebook.py +++ b/gui/wxpython/main_window/notebook.py @@ -25,6 +25,7 @@ from gui_core.wrap import SimpleTabArt from mapdisp.frame import MapPanel + class MainPageFrame(wx.Frame): """Frame for independent map display window.""" @@ -37,9 +38,7 @@ def __init__(self, parent, panel, size, pos, title, icon="grass"): # set system icon self.SetIcon( - wx.Icon( - os.path.join(globalvar.ICONDIR, icon + ".ico"), wx.BITMAP_TYPE_ICO - ) + wx.Icon(os.path.join(globalvar.ICONDIR, icon + ".ico"), wx.BITMAP_TYPE_ICO) ) self.panel.onFocus.emit() @@ -69,6 +68,7 @@ def OnClose(self, event): else: self.panel.OnCloseWindow(event=None) + class MainNotebook(aui.AuiNotebook): """Main notebook class. Overrides some AuiNotebook classes. Takes into consideration the dock/undock functionality. @@ -105,7 +105,7 @@ def UndockPage(self, page): size=original_size, pos=original_pos, title=text, - icon=icon + icon=icon, ) frame.SetDockingCallback(self.DockPage) diff --git a/gui/wxpython/main_window/page.py b/gui/wxpython/main_window/page.py index d8e5aa5ef68..1c08ccfe9e0 100644 --- a/gui/wxpython/main_window/page.py +++ b/gui/wxpython/main_window/page.py @@ -17,10 +17,11 @@ from grass.pydispatch.signal import Signal + class MainPageBase: def __init__(self, dockable): self._mainnotebook = None - + self.canCloseCallback = None # distinquishes whether map panel is dockable (Single-Window) @@ -34,7 +35,7 @@ def __init__(self, dockable): # 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") @@ -43,9 +44,7 @@ def __init__(self, dockable): def _pgnumDict(self): """Get dictionary containg page index""" - return { - "mainnotebook": self._mainnotebook.GetPageIndex(self) - } + return {"mainnotebook": self._mainnotebook.GetPageIndex(self)} def SetUpPage(self, parent, notebook, can_close=None): self._mainnotebook = notebook @@ -60,7 +59,7 @@ def CanClosePage(): # bind various events self.closingPage.connect(parent._closePageNoEvent) self.renamingPage.connect(parent._renamePageNoEvent) - + def SetDockingCallback(self, function): """Sets docking bound method to dock or undock""" self._docking_callback = function diff --git a/gui/wxpython/mapdisp/frame.py b/gui/wxpython/mapdisp/frame.py index 05874a9d842..8af419e809a 100644 --- a/gui/wxpython/mapdisp/frame.py +++ b/gui/wxpython/mapdisp/frame.py @@ -968,9 +968,7 @@ def OnCloseWindow(self, event, askIfSaveWorkspace=True): """ Debug.msg(2, "MapPanel.OnCloseWindow()") if self.canCloseCallback: - pgnum_dict = self.canCloseCallback( - askIfSaveWorkspace=askIfSaveWorkspace - ) + pgnum_dict = self.canCloseCallback(askIfSaveWorkspace=askIfSaveWorkspace) if pgnum_dict is not None: self.CleanUp() if pgnum_dict["layers"] > -1: From a6089f33137bd21a2c5b9b45c02c66753cced3d5 Mon Sep 17 00:00:00 2001 From: Martin Landa Date: Wed, 8 Nov 2023 12:16:45 +0100 Subject: [PATCH 21/51] fix multiwindow layout --- gui/wxpython/lmgr/frame.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gui/wxpython/lmgr/frame.py b/gui/wxpython/lmgr/frame.py index 7a90be4b1d6..8e83ba7b1b5 100644 --- a/gui/wxpython/lmgr/frame.py +++ b/gui/wxpython/lmgr/frame.py @@ -759,9 +759,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() From 0fb7a94f07fe5b2eccbe16f569c3cc03dfa299f9 Mon Sep 17 00:00:00 2001 From: Martin Landa Date: Wed, 8 Nov 2023 13:11:20 +0100 Subject: [PATCH 22/51] remove unused import --- gui/wxpython/gmodeler/frame.py | 1 - 1 file changed, 1 deletion(-) diff --git a/gui/wxpython/gmodeler/frame.py b/gui/wxpython/gmodeler/frame.py index d22f33dd3a3..293b51b9ecd 100644 --- a/gui/wxpython/gmodeler/frame.py +++ b/gui/wxpython/gmodeler/frame.py @@ -16,7 +16,6 @@ """ import os -import sys import wx From 3f9077383d22c03316c9c57ce8cc364370acae88 Mon Sep 17 00:00:00 2001 From: Martin Landa Date: Wed, 8 Nov 2023 13:25:13 +0100 Subject: [PATCH 23/51] remove unused import --- gui/wxpython/gmodeler/panels.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/wxpython/gmodeler/panels.py b/gui/wxpython/gmodeler/panels.py index bea54d86696..70fe498cdb6 100644 --- a/gui/wxpython/gmodeler/panels.py +++ b/gui/wxpython/gmodeler/panels.py @@ -44,7 +44,7 @@ from core.debug import Debug from core.gcmd import GMessage, GException, GWarning, GError from core.settings import UserSettings -from core.giface import Notification, StandaloneGrassInterface +from core.giface import Notification from gui_core.widgets import GNotebook from gui_core.goutput import GConsoleWindow From a18219995a30fd5266fb7558a093fdc24d020266 Mon Sep 17 00:00:00 2001 From: Martin Landa Date: Wed, 8 Nov 2023 13:49:57 +0100 Subject: [PATCH 24/51] remove unused import --- gui/wxpython/gmodeler/panels.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gui/wxpython/gmodeler/panels.py b/gui/wxpython/gmodeler/panels.py index 70fe498cdb6..90a0e5f88a1 100644 --- a/gui/wxpython/gmodeler/panels.py +++ b/gui/wxpython/gmodeler/panels.py @@ -72,10 +72,9 @@ from gmodeler.preferences import PreferencesDialog, PropertiesDialog wxModelDone, EVT_MODEL_DONE = NewEvent() -0 + from grass.script.utils import try_remove from grass.script import core as grass -from grass.pydispatch.signal import Signal class ModelerPanel(wx.Panel, MainPageBase): From 4b7e5f5db62ac2996293b960b28c69042542cb00 Mon Sep 17 00:00:00 2001 From: Martin Landa Date: Wed, 8 Nov 2023 14:20:04 +0100 Subject: [PATCH 25/51] disable E1101 --- gui/wxpython/main_window/page.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gui/wxpython/main_window/page.py b/gui/wxpython/main_window/page.py index 1c08ccfe9e0..50d0835e197 100644 --- a/gui/wxpython/main_window/page.py +++ b/gui/wxpython/main_window/page.py @@ -86,13 +86,13 @@ def _onCloseWindow(self, event): pgnum_dict=pgnum_dict, is_docked=self.IsDocked() ) if not self.IsDocked(): - frame = self.GetParent() + frame = self.GetParent() # pylint: disable=E1101 frame.Destroy() else: self.closingPage.emit(pgnum_dict=pgnum_dict) # Destroy is called when notebook page is deleted else: - self.parent.Destroy() + self.parent.Destroy() # pylint: disable=E1101 def RenamePage(self, title): """Rename page or change frame title""" @@ -104,6 +104,6 @@ def RenamePage(self, title): pgnum_dict=pgnum_dict, is_docked=self.IsDocked(), text=title ) if not self.IsDocked(): - self.GetParent().SetTitle(title) + self.GetParent().SetTitle(title) # pylint: disable=E1101 else: - self.GetParent().SetTitle(title) + self.GetParent().SetTitle(title) # pylint: disable=E1101 From c1a7959dec45130d18bb3b36f099b598757f06ce Mon Sep 17 00:00:00 2001 From: Martin Landa Date: Wed, 8 Nov 2023 14:25:50 +0100 Subject: [PATCH 26/51] black applied --- gui/wxpython/main_window/page.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gui/wxpython/main_window/page.py b/gui/wxpython/main_window/page.py index 50d0835e197..51a619b9541 100644 --- a/gui/wxpython/main_window/page.py +++ b/gui/wxpython/main_window/page.py @@ -86,13 +86,13 @@ def _onCloseWindow(self, event): pgnum_dict=pgnum_dict, is_docked=self.IsDocked() ) if not self.IsDocked(): - frame = self.GetParent() # pylint: disable=E1101 + frame = self.GetParent() # pylint: disable=E1101 frame.Destroy() else: self.closingPage.emit(pgnum_dict=pgnum_dict) # Destroy is called when notebook page is deleted else: - self.parent.Destroy() # pylint: disable=E1101 + self.parent.Destroy() # pylint: disable=E1101 def RenamePage(self, title): """Rename page or change frame title""" From 38611d87f0e354b595d29a3ed2a8162577b8efb7 Mon Sep 17 00:00:00 2001 From: Martin Landa Date: Wed, 8 Nov 2023 15:20:12 +0100 Subject: [PATCH 27/51] pylint: change from inline solution to .pyintrc --- gui/wxpython/.pylintrc | 2 +- gui/wxpython/main_window/page.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/gui/wxpython/.pylintrc b/gui/wxpython/.pylintrc index 6268e8467a7..dd2d7e6acd9 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.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/main_window/page.py b/gui/wxpython/main_window/page.py index 51a619b9541..1c08ccfe9e0 100644 --- a/gui/wxpython/main_window/page.py +++ b/gui/wxpython/main_window/page.py @@ -86,13 +86,13 @@ def _onCloseWindow(self, event): pgnum_dict=pgnum_dict, is_docked=self.IsDocked() ) if not self.IsDocked(): - frame = self.GetParent() # pylint: disable=E1101 + 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() # pylint: disable=E1101 + self.parent.Destroy() def RenamePage(self, title): """Rename page or change frame title""" @@ -104,6 +104,6 @@ def RenamePage(self, title): pgnum_dict=pgnum_dict, is_docked=self.IsDocked(), text=title ) if not self.IsDocked(): - self.GetParent().SetTitle(title) # pylint: disable=E1101 + self.GetParent().SetTitle(title) else: - self.GetParent().SetTitle(title) # pylint: disable=E1101 + self.GetParent().SetTitle(title) From f49a47379da5af03ad08467375a9f82a4d9d74aa Mon Sep 17 00:00:00 2001 From: Martin Landa Date: Wed, 8 Nov 2023 17:15:37 +0100 Subject: [PATCH 28/51] fix pyint ignored-classes --- gui/wxpython/.pylintrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/wxpython/.pylintrc b/gui/wxpython/.pylintrc index dd2d7e6acd9..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,main_window.MainPageBase +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 From 0be08d31b91c3ca1e5b0e080ad1c109a74c39be6 Mon Sep 17 00:00:00 2001 From: Martin Landa Date: Sat, 2 Mar 2024 14:16:50 +0100 Subject: [PATCH 29/51] merge manually a71b72a --- gui/wxpython/gmodeler/panels.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gui/wxpython/gmodeler/panels.py b/gui/wxpython/gmodeler/panels.py index 90a0e5f88a1..150a1d5ac77 100644 --- a/gui/wxpython/gmodeler/panels.py +++ b/gui/wxpython/gmodeler/panels.py @@ -658,7 +658,7 @@ def WriteModelFile(self, filename): tmpfile.seek(0) for line in tmpfile.readlines(): mfile.write(line) - except IOError: + except OSError: wx.MessageBox( parent=self, message=_("Unable to open file <%s> for writing.") % filename, @@ -1712,7 +1712,7 @@ def OnRun(self, event): try: fd = open(self.filename, "w") fd.write(self.body.GetText()) - except IOError as e: + except OSError as e: GError(_("Unable to launch Python script. %s") % e, parent=self) return finally: From 42840665eb6cf068d2359725a89060a15b0e5f4c Mon Sep 17 00:00:00 2001 From: Martin Landa Date: Sat, 2 Mar 2024 18:41:16 +0100 Subject: [PATCH 30/51] merge manually f393525 --- gui/wxpython/gmodeler/panels.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/wxpython/gmodeler/panels.py b/gui/wxpython/gmodeler/panels.py index 150a1d5ac77..dcd7a207fe8 100644 --- a/gui/wxpython/gmodeler/panels.py +++ b/gui/wxpython/gmodeler/panels.py @@ -135,7 +135,7 @@ def __init__( self.pythonPanel = PythonPanel(parent=self) - self._gconsole = GConsole(guiparent=self) + self._gconsole = GConsole(guiparent=self, giface=giface) self.goutput = GConsoleWindow( parent=self, giface=giface, gconsole=self._gconsole ) From e5c9482fcc7309ba04a3255d63d0eae430221e73 Mon Sep 17 00:00:00 2001 From: Martin Landa Date: Sat, 2 Mar 2024 19:47:29 +0100 Subject: [PATCH 31/51] merge manually e07531f --- gui/wxpython/gmodeler/canvas.py | 13 ++++++------- gui/wxpython/gmodeler/panels.py | 7 ++++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/gui/wxpython/gmodeler/canvas.py b/gui/wxpython/gmodeler/canvas.py index 55947bee1ab..ce3a7cd91a1 100644 --- a/gui/wxpython/gmodeler/canvas.py +++ b/gui/wxpython/gmodeler/canvas.py @@ -81,14 +81,13 @@ def GetNewShapePos(self, yoffset=50): :return: x,y """ - diagram = self.GetDiagram() - if diagram.GetShapeList(): - last = diagram.GetShapeList()[-1] - y = last.GetY() + last.GetBoundingBoxMin()[1] - else: - y = 20 + ymax = 20 + for item in self.GetDiagram().GetShapeList(): + y = item.GetY() + item.GetBoundingBoxMin()[1] + if y > ymax: + ymax = y - return (self.GetSize()[0] // 2, y + yoffset) + return (self.GetSize()[0] // 2, ymax + yoffset) def GetShapesSelected(self): """Get list of selected shapes""" diff --git a/gui/wxpython/gmodeler/panels.py b/gui/wxpython/gmodeler/panels.py index dcd7a207fe8..2e1569b5cb7 100644 --- a/gui/wxpython/gmodeler/panels.py +++ b/gui/wxpython/gmodeler/panels.py @@ -522,11 +522,12 @@ def GetOptData(self, dcmd, layer, params, propwin): # arrange data items if data_items: dc = wx.ClientDC(self.canvas) - p = 360 / len(data_items) - r = 200 + 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 + r * math.sin(alpha), y + r * math.cos(alpha)) + data.Move(dc, x + rx * math.sin(alpha), y + ry * math.cos(alpha)) alpha += p * (math.pi / 180) data.Show(True) From 610d04235b3a24209fa1f4b0e0b9fda66924bc3d Mon Sep 17 00:00:00 2001 From: Martin Landa Date: Sat, 2 Mar 2024 19:51:55 +0100 Subject: [PATCH 32/51] merge manually 0ad33cc --- gui/wxpython/gmodeler/panels.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/gui/wxpython/gmodeler/panels.py b/gui/wxpython/gmodeler/panels.py index 2e1569b5cb7..b50c352442c 100644 --- a/gui/wxpython/gmodeler/panels.py +++ b/gui/wxpython/gmodeler/panels.py @@ -1643,7 +1643,13 @@ def RefreshScript(self): return False fd = tempfile.TemporaryFile(mode="r+") - self.write_object(fd, self.parent.GetModel()) + 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() From 4f45eadfb53c39006204464ec82f847b28c3bbb3 Mon Sep 17 00:00:00 2001 From: Martin Landa Date: Sat, 2 Mar 2024 19:56:34 +0100 Subject: [PATCH 33/51] merge manually 76c1ac5 --- gui/wxpython/gmodeler/canvas.py | 10 +++++++++- gui/wxpython/gmodeler/panels.py | 11 ++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/gui/wxpython/gmodeler/canvas.py b/gui/wxpython/gmodeler/canvas.py index ce3a7cd91a1..d10c8b4e0ee 100644 --- a/gui/wxpython/gmodeler/canvas.py +++ b/gui/wxpython/gmodeler/canvas.py @@ -175,7 +175,15 @@ def OnProperties(self, event=None): ) elif isinstance(shape, ModelData): - if shape.GetPrompt() in ("raster", "vector", "raster_3d"): + if shape.GetPrompt() in ( + "raster", + "vector", + "raster_3d", + "stds", + "strds", + "stvds", + "str3ds", + ): dlg = ModelDataDialog(parent=self.frame, shape=shape) shape.SetPropDialog(dlg) dlg.CentreOnParent() diff --git a/gui/wxpython/gmodeler/panels.py b/gui/wxpython/gmodeler/panels.py index b50c352442c..130e424fc05 100644 --- a/gui/wxpython/gmodeler/panels.py +++ b/gui/wxpython/gmodeler/panels.py @@ -438,6 +438,10 @@ def GetOptData(self, dcmd, layer, params, propwin): "vector", "raster_3d", "dbtable", + "stds", + "strds", + "stvds", + "str3ds", ): continue @@ -473,7 +477,12 @@ def GetOptData(self, dcmd, layer, params, propwin): data.Update() continue - data = ModelData( + dataClass = ( + ModelDataSeries + if p.get("prompt", "").startswith("st") + else ModelDataSingle + ) + data = dataClass( self, value=p.get("value", ""), prompt=p.get("prompt", ""), From d816fdd7e38a5b8b3e3a3c987fbc7fb07e49d883 Mon Sep 17 00:00:00 2001 From: Martin Landa Date: Sat, 2 Mar 2024 21:11:48 +0100 Subject: [PATCH 34/51] disable ModelChanged() in OnSize() when window is dockable --- gui/wxpython/gmodeler/panels.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gui/wxpython/gmodeler/panels.py b/gui/wxpython/gmodeler/panels.py index 130e424fc05..42987d5f399 100644 --- a/gui/wxpython/gmodeler/panels.py +++ b/gui/wxpython/gmodeler/panels.py @@ -335,7 +335,10 @@ def time_elapsed(etime): def OnSize(self, event): """Window resized, save to the model""" - self.ModelChanged() + if not self._dockable: + # model changed: window resizing is applied only if the + # window is not dockable + self.ModelChanged() event.Skip() def _deleteIntermediateData(self): From bbeb2724e5925e3a90fc06e24fdc2412a667b199 Mon Sep 17 00:00:00 2001 From: Martin Landa Date: Sat, 2 Mar 2024 23:55:46 +0100 Subject: [PATCH 35/51] use IsDockable() instead of _dockable --- gui/wxpython/gmodeler/panels.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/wxpython/gmodeler/panels.py b/gui/wxpython/gmodeler/panels.py index 42987d5f399..32b00b5b36a 100644 --- a/gui/wxpython/gmodeler/panels.py +++ b/gui/wxpython/gmodeler/panels.py @@ -335,7 +335,7 @@ def time_elapsed(etime): def OnSize(self, event): """Window resized, save to the model""" - if not self._dockable: + if not self.IsDockable(): # model changed: window resizing is applied only if the # window is not dockable self.ModelChanged() From 33e8219368a777da4f12059a18e2f49ceaf58e1a Mon Sep 17 00:00:00 2001 From: Martin Landa Date: Sun, 3 Mar 2024 00:04:53 +0100 Subject: [PATCH 36/51] hide shortcuts when window is dockable --- gui/wxpython/gmodeler/toolbars.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/gui/wxpython/gmodeler/toolbars.py b/gui/wxpython/gmodeler/toolbars.py index 8bf99161f88..c959ca69d6e 100644 --- a/gui/wxpython/gmodeler/toolbars.py +++ b/gui/wxpython/gmodeler/toolbars.py @@ -40,11 +40,24 @@ def __init__(self, parent): def _toolbarData(self): """Toolbar data""" + # dockable window has no menu, so shortcuts doesn't work when + # window is dockable + show_shortcuts = not self.parent.IsDockable() + 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+N)" if show_shortcuts else ""), + ), + "open": MetaIcon( + img="open", + label=_("Load model from file") + + (" (Ctrl+O)" if show_shortcuts else ""), + ), "save": MetaIcon( - img="save", label=_("Save current model to file (Ctrl+S)") + img="save", + label=_("Save current model to file") + + (" (Ctrl+S)" if show_shortcuts else ""), ), "toImage": MetaIcon(img="image-export", label=_("Export model to image")), "toPython": MetaIcon( From eb918834b143b455e2e946e92f50ea3d484140a0 Mon Sep 17 00:00:00 2001 From: Martin Landa Date: Sun, 3 Mar 2024 15:30:45 +0100 Subject: [PATCH 37/51] menu initial implementation --- gui/wxpython/gmodeler/frame.py | 6 ++- gui/wxpython/gmodeler/panels.py | 8 ++-- gui/wxpython/gui_core/menu.py | 57 ++++++++++++++++++++++----- gui/wxpython/main_window/frame.py | 13 ++++-- gui/wxpython/main_window/notebook.py | 26 ++++++++++-- gui/wxpython/main_window/page.py | 16 +++++++- gui/wxpython/xml/menudata_modeler.xml | 44 ++++++++++----------- 7 files changed, 125 insertions(+), 45 deletions(-) diff --git a/gui/wxpython/gmodeler/frame.py b/gui/wxpython/gmodeler/frame.py index d0e5e4e89c1..5ad7b6c34e0 100644 --- a/gui/wxpython/gmodeler/frame.py +++ b/gui/wxpython/gmodeler/frame.py @@ -48,10 +48,12 @@ def __init__( self.panel = ModelerPanel(parent=self, giface=giface, statusbar=self.statusbar) self.menubar = Menubar( - parent=self, model=ModelerMenuData().GetModel(separators=True) + parent=self, + model=ModelerMenuData().GetModel(separators=True), + class_handler=self.panel, ) self.SetMenuBar(self.menubar) self.SetName("ModelerFrame") self.SetMinSize((640, 300)) - self.SetSize((800, 600)) \ No newline at end of file + self.SetSize((800, 600)) diff --git a/gui/wxpython/gmodeler/panels.py b/gui/wxpython/gmodeler/panels.py index 32b00b5b36a..c7d6c64359c 100644 --- a/gui/wxpython/gmodeler/panels.py +++ b/gui/wxpython/gmodeler/panels.py @@ -774,7 +774,7 @@ def DefineCondition(self, condition): def OnModelNew(self, event): """Create new model""" - Debug.msg(4, "ModelFrame.OnModelNew():") + Debug.msg(4, "ModelerPanel.OnModelNew():") # ask user to save current model if self.modelFile and self.modelChanged: @@ -828,7 +828,7 @@ def OnModelOpen(self, event): if not filename: return - Debug.msg(4, "ModelFrame.OnModelOpen(): filename=%s" % filename) + Debug.msg(4, "ModelerPanel.OnModelOpen(): filename=%s" % filename) # close current model self.OnModelClose() @@ -862,7 +862,7 @@ def OnModelSave(self, event=None): if dlg.ShowModal() == wx.ID_NO: dlg.Destroy() else: - Debug.msg(4, "ModelFrame.OnModelSave(): filename=%s" % self.modelFile) + Debug.msg(4, "ModelerPanel.OnModelSave(): filename=%s" % self.modelFile) self.WriteModelFile(self.modelFile) self.SetStatusText(_("File <%s> saved") % self.modelFile, 0) self.RenamePage( @@ -916,7 +916,7 @@ def OnModelSaveAs(self, event=None): def OnModelClose(self, event=None): """Close model file""" - Debug.msg(4, "ModelFrame.OnModelClose(): file=%s" % self.modelFile) + Debug.msg(4, "ModelerPanel.OnModelClose(): file=%s" % self.modelFile) # ask user to save current model if self.modelFile and self.modelChanged: self.OnModelSave() diff --git a/gui/wxpython/gui_core/menu.py b/gui/wxpython/gui_core/menu.py index d616b73235c..5470635672e 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) 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/main_window/frame.py b/gui/wxpython/main_window/frame.py index d75d84a511d..9fc59d71585 100644 --- a/gui/wxpython/main_window/frame.py +++ b/gui/wxpython/main_window/frame.py @@ -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, MenuItem as GMenuItem from core.debug import Debug from lmgr.toolbars import LMWorkspaceToolbar, LMToolsToolbar from lmgr.toolbars import LMMiscToolbar, LMNvizToolbar, DisplayPanelToolbar @@ -876,11 +875,19 @@ def OnGCPManager(self, event=None, cmd=None): def OnGModeler(self, event=None, cmd=None): """Launch Graphical Modeler. See OnIClass documentation""" from gmodeler.panels import ModelerPanel + from gmodeler.menudata import ModelerMenuData gmodeler_panel = ModelerPanel( parent=self, giface=self._giface, statusbar=self.statusbar, dockable=True ) - gmodeler_panel.SetUpPage(self, self.mainnotebook) + gmodeler_menu = GMenuItem( + parent=self, + model=ModelerMenuData().GetModel(separators=True), + class_handler=gmodeler_panel, + ) + gmodeler_panel.SetUpPage( + self, self.mainnotebook, menu=gmodeler_menu, menuName="&Graphical Modeler" + ) # add map display panel to notebook and make it current self.mainnotebook.AddPage(gmodeler_panel, _("Graphical Modeler")) diff --git a/gui/wxpython/main_window/notebook.py b/gui/wxpython/main_window/notebook.py index 9b4843d28c9..9470738b0b2 100644 --- a/gui/wxpython/main_window/notebook.py +++ b/gui/wxpython/main_window/notebook.py @@ -85,12 +85,30 @@ 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) + # 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 self.GetCurrentPage().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(mbar.GetMenuCount() - 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) diff --git a/gui/wxpython/main_window/page.py b/gui/wxpython/main_window/page.py index 1c08ccfe9e0..4e77892d1de 100644 --- a/gui/wxpython/main_window/page.py +++ b/gui/wxpython/main_window/page.py @@ -22,6 +22,10 @@ class MainPageBase: def __init__(self, dockable): self._mainnotebook = None + # menu associated with the panel + self._menu = None + self._menuName = None + self.canCloseCallback = None # distinquishes whether map panel is dockable (Single-Window) @@ -46,7 +50,7 @@ def _pgnumDict(self): """Get dictionary containg page index""" return {"mainnotebook": self._mainnotebook.GetPageIndex(self)} - def SetUpPage(self, parent, notebook, can_close=None): + def SetUpPage(self, parent, notebook, can_close=None, menu=None, menuName=None): self._mainnotebook = notebook def CanClosePage(): @@ -60,6 +64,10 @@ def CanClosePage(): self.closingPage.connect(parent._closePageNoEvent) self.renamingPage.connect(parent._renamePageNoEvent) + # set up menu if defined + self._menu = menu + self._menuName = menuName + def SetDockingCallback(self, function): """Sets docking bound method to dock or undock""" self._docking_callback = function @@ -107,3 +115,9 @@ def RenamePage(self, title): self.GetParent().SetTitle(title) else: self.GetParent().SetTitle(title) + + def HasMenu(self): + return self._menu is not None + + def GetMenu(self): + return self._menu, self._menuName diff --git a/gui/wxpython/xml/menudata_modeler.xml b/gui/wxpython/xml/menudata_modeler.xml index 7c909a9ee1e..bf2bd61fe62 100644 --- a/gui/wxpython/xml/menudata_modeler.xml +++ b/gui/wxpython/xml/menudata_modeler.xml @@ -6,48 +6,48 @@ Create new model - panel.OnModelNew + OnModelNew Ctrl+N Load model from file - panel.OnModelOpen + OnModelOpen Ctrl+O Save model - panel.OnModelSave + OnModelSave Ctrl+S Save model to file - panel.OnModelSaveAs + OnModelSaveAs Close model file - panel.OnModelClose + OnModelClose Export model to image - panel.OnExportImage + OnExportImage Export model to Python script - panel.OnExportPython + OnExportPython Ctrl+P Close modeler window - panel.OnCloseWindow + OnCloseWindow Ctrl+W @@ -58,7 +58,7 @@ Modeler settings - panel.OnPreferences + OnPreferences @@ -68,66 +68,66 @@ Add action (GRASS command) to model - panel.OnAddAction + OnAddAction Ctrl+A Add data item to model - panel.OnAddData + OnAddData Ctrl+D Define relation between data and action items - panel.OnDefineRelation + OnDefineRelation Adds loop (series) to model - panel.OnDefineLoop + OnDefineLoop Ctrl+L Adds condition (if/else) to model - panel.OnDefineCondition + OnDefineCondition Ctrl+I Adds comment to model - panel.OnAddComment + OnAddComment Ctrl+# Remove action/data from model - panel.OnRemoveItem + OnRemoveItem Model properties (name, purpose, etc.) - panel.OnModelProperties + OnModelProperties Delete intermediate data defined in the model - panel.OnDeleteData + OnDeleteData Run entire model - panel.OnRunModel + OnRunModel Ctrl+R Validate entire model - panel.OnValidateModel + OnValidateModel @@ -137,13 +137,13 @@ Display the HTML man pages of Graphical modeler - panel.OnHelp + OnHelp Display information about Graphical Modeler - panel.OnAbout + OnAbout From a19672d86e2fcee9bf9b2aa46420473353698e9a Mon Sep 17 00:00:00 2001 From: Martin Landa Date: Sun, 3 Mar 2024 16:03:20 +0100 Subject: [PATCH 38/51] one word menu item --- gui/wxpython/main_window/frame.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/wxpython/main_window/frame.py b/gui/wxpython/main_window/frame.py index 9fc59d71585..bc72dd47011 100644 --- a/gui/wxpython/main_window/frame.py +++ b/gui/wxpython/main_window/frame.py @@ -886,7 +886,7 @@ def OnGModeler(self, event=None, cmd=None): class_handler=gmodeler_panel, ) gmodeler_panel.SetUpPage( - self, self.mainnotebook, menu=gmodeler_menu, menuName="&Graphical Modeler" + self, self.mainnotebook, menu=gmodeler_menu, menuName="&Modeler" ) # add map display panel to notebook and make it current From 14e98354459abd0e4dc15d986ab1619114e63d1b Mon Sep 17 00:00:00 2001 From: Martin Landa Date: Sun, 3 Mar 2024 16:03:38 +0100 Subject: [PATCH 39/51] fix menu replace --- gui/wxpython/main_window/notebook.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gui/wxpython/main_window/notebook.py b/gui/wxpython/main_window/notebook.py index 9470738b0b2..c0040caadb5 100644 --- a/gui/wxpython/main_window/notebook.py +++ b/gui/wxpython/main_window/notebook.py @@ -102,9 +102,11 @@ def OnPageChanged(self, event): menu, menuName = page.GetMenu() if mbar.GetMenuCount() == self._menuCount: appendMenu = mbar.Insert + idx = mbar.GetMenuCount() - 1 else: appendMenu = mbar.Replace - appendMenu(mbar.GetMenuCount() - 1, menu, menuName) + idx = mbar.GetMenuCount() - 2 + appendMenu(idx, menu, menuName) elif mbar.GetMenuCount() > self._menuCount: # remove additional menu item mbar.Remove(self._menuCount - 1) From dc25c86a59af45a224fd061a5d74013b41102525 Mon Sep 17 00:00:00 2001 From: Martin Landa Date: Sun, 3 Mar 2024 16:05:05 +0100 Subject: [PATCH 40/51] menu index simplified --- gui/wxpython/main_window/notebook.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/gui/wxpython/main_window/notebook.py b/gui/wxpython/main_window/notebook.py index c0040caadb5..cbad3ccd6fc 100644 --- a/gui/wxpython/main_window/notebook.py +++ b/gui/wxpython/main_window/notebook.py @@ -102,11 +102,9 @@ def OnPageChanged(self, event): menu, menuName = page.GetMenu() if mbar.GetMenuCount() == self._menuCount: appendMenu = mbar.Insert - idx = mbar.GetMenuCount() - 1 else: appendMenu = mbar.Replace - idx = mbar.GetMenuCount() - 2 - appendMenu(idx, menu, menuName) + appendMenu(self._menuCount - 1, menu, menuName) elif mbar.GetMenuCount() > self._menuCount: # remove additional menu item mbar.Remove(self._menuCount - 1) From ccc39f8cf73a700e3a3cf727d1c4b53e1f0b91c8 Mon Sep 17 00:00:00 2001 From: Martin Landa Date: Sun, 3 Mar 2024 17:54:38 +0100 Subject: [PATCH 41/51] unique menu shortcuts --- gui/wxpython/gmodeler/toolbars.py | 28 ++++++++++++++++----------- gui/wxpython/xml/menudata_modeler.xml | 22 ++++++++++----------- 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/gui/wxpython/gmodeler/toolbars.py b/gui/wxpython/gmodeler/toolbars.py index c959ca69d6e..1c809ba2a8e 100644 --- a/gui/wxpython/gmodeler/toolbars.py +++ b/gui/wxpython/gmodeler/toolbars.py @@ -47,33 +47,39 @@ def _toolbarData(self): icons = { "new": MetaIcon( img="create", - label=_("Create new model") + (" (Ctrl+N)" if show_shortcuts else ""), + label=_("Create new model") + " (Ctrl+Alt+N)", ), "open": MetaIcon( img="open", - label=_("Load model from file") - + (" (Ctrl+O)" if show_shortcuts else ""), + label=_("Load model from file") + " (Ctrl+Alt+O)", ), "save": MetaIcon( img="save", - label=_("Save current model to file") - + (" (Ctrl+S)" if show_shortcuts else ""), + 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": MetaIcon(img="modeler-settings", label=_("Modeler settings")), "properties": MetaIcon(img="options", label=_("Set model properties")), 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 From 82e88179496d6369dabbcd5a2fbb27ce3f80cc5a Mon Sep 17 00:00:00 2001 From: Martin Landa Date: Sun, 3 Mar 2024 18:32:40 +0100 Subject: [PATCH 42/51] fix menu for undocked windows --- gui/wxpython/main_window/frame.py | 12 +++++------- gui/wxpython/main_window/notebook.py | 12 +++++++++--- gui/wxpython/main_window/page.py | 27 +++++++++++++++++++++++---- 3 files changed, 37 insertions(+), 14 deletions(-) diff --git a/gui/wxpython/main_window/frame.py b/gui/wxpython/main_window/frame.py index bc72dd47011..60fe7ba935d 100644 --- a/gui/wxpython/main_window/frame.py +++ b/gui/wxpython/main_window/frame.py @@ -69,7 +69,7 @@ MapLayersDialog, QuitDialog, ) -from gui_core.menu import SearchModuleWindow, Menu as GMenu, MenuItem as GMenuItem +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 @@ -880,13 +880,11 @@ def OnGModeler(self, event=None, cmd=None): gmodeler_panel = ModelerPanel( parent=self, giface=self._giface, statusbar=self.statusbar, dockable=True ) - gmodeler_menu = GMenuItem( - parent=self, - model=ModelerMenuData().GetModel(separators=True), - class_handler=gmodeler_panel, - ) gmodeler_panel.SetUpPage( - self, self.mainnotebook, menu=gmodeler_menu, menuName="&Modeler" + self, + self.mainnotebook, + menuModel=ModelerMenuData().GetModel(separators=True), + menuName="&Modeler", ) # add map display panel to notebook and make it current diff --git a/gui/wxpython/main_window/notebook.py b/gui/wxpython/main_window/notebook.py index cbad3ccd6fc..e9df4db5771 100644 --- a/gui/wxpython/main_window/notebook.py +++ b/gui/wxpython/main_window/notebook.py @@ -27,9 +27,9 @@ class MainPageFrame(wx.Frame): - """Frame for independent map display window.""" + """Frame for independent window.""" - def __init__(self, parent, panel, size, pos, title, icon="grass"): + 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.panel = panel self.panel.Reparent(self) @@ -41,6 +41,9 @@ def __init__(self, parent, panel, size, pos, title, icon="grass"): wx.Icon(os.path.join(globalvar.ICONDIR, icon + ".ico"), wx.BITMAP_TYPE_ICO) ) + if menu is not None: + self.SetMenuBar(menu) + self.panel.onFocus.emit() self.Bind(wx.EVT_CLOSE, self.OnClose) @@ -97,7 +100,7 @@ def OnPageChanged(self, event): # set up menu mbar = self.parent.menubar - if self.GetCurrentPage().HasMenu(): + 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: @@ -116,6 +119,8 @@ def UndockPage(self, page): original_size = page.GetSize() original_pos = page.GetPosition() icon = "grass_map" if isinstance(page, MapPanel) else "grass" + if page.HasMenu(): + menu, _ = page.GetMenu() self.RemovePage(index) frame = MainPageFrame( parent=self.parent, @@ -124,6 +129,7 @@ def UndockPage(self, page): pos=original_pos, title=text, icon=icon, + menu=menu, ) frame.SetDockingCallback(self.DockPage) diff --git a/gui/wxpython/main_window/page.py b/gui/wxpython/main_window/page.py index 4e77892d1de..404c3f7b913 100644 --- a/gui/wxpython/main_window/page.py +++ b/gui/wxpython/main_window/page.py @@ -16,6 +16,7 @@ """ from grass.pydispatch.signal import Signal +from gui_core.menu import MenuItem as GMenuItem, Menu as GMenu class MainPageBase: @@ -24,6 +25,7 @@ def __init__(self, dockable): # menu associated with the panel self._menu = None + self._menuModel = None self._menuName = None self.canCloseCallback = None @@ -50,7 +52,9 @@ def _pgnumDict(self): """Get dictionary containg page index""" return {"mainnotebook": self._mainnotebook.GetPageIndex(self)} - def SetUpPage(self, parent, notebook, can_close=None, menu=None, menuName=None): + def SetUpPage( + self, parent, notebook, can_close=None, menuModel=None, menuName=None + ): self._mainnotebook = notebook def CanClosePage(): @@ -65,7 +69,7 @@ def CanClosePage(): self.renamingPage.connect(parent._renamePageNoEvent) # set up menu if defined - self._menu = menu + self._menuModel = menuModel self._menuName = menuName def SetDockingCallback(self, function): @@ -117,7 +121,22 @@ def RenamePage(self, title): self.GetParent().SetTitle(title) def HasMenu(self): - return self._menu is not None + """Check if menu is defined. + + :return True if menu defined otherwise False + """ + return self._menuModel is not None def GetMenu(self): - return self._menu, self._menuName + """Get menu object if defined. + + :return: menu object (Menu for undocked window, MenuItem for docked window) + """ + menu = None + if self._menuModel is not None: + menuClass = GMenuItem if self._docked else GMenu + menu = menuClass( + parent=self.parent, model=self._menuModel, class_handler=self + ) + + return menu, self._menuName From 33b11199900cb5c53a30f18dc3aa2a164cb475f9 Mon Sep 17 00:00:00 2001 From: Martin Landa Date: Sun, 3 Mar 2024 21:51:12 +0100 Subject: [PATCH 43/51] reuse menu objects if already existing --- gui/wxpython/main_window/notebook.py | 5 +++++ gui/wxpython/main_window/page.py | 15 +++++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/gui/wxpython/main_window/notebook.py b/gui/wxpython/main_window/notebook.py index e9df4db5771..77b37ada1ea 100644 --- a/gui/wxpython/main_window/notebook.py +++ b/gui/wxpython/main_window/notebook.py @@ -121,6 +121,8 @@ def UndockPage(self, page): icon = "grass_map" if isinstance(page, MapPanel) else "grass" if page.HasMenu(): menu, _ = page.GetMenu() + else: + menu = None self.RemovePage(index) frame = MainPageFrame( parent=self.parent, @@ -139,6 +141,9 @@ def DockPage(self, page): page.Reparent(self) 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): diff --git a/gui/wxpython/main_window/page.py b/gui/wxpython/main_window/page.py index 404c3f7b913..a56d424dba9 100644 --- a/gui/wxpython/main_window/page.py +++ b/gui/wxpython/main_window/page.py @@ -23,8 +23,8 @@ class MainPageBase: def __init__(self, dockable): self._mainnotebook = None - # menu associated with the panel - self._menu = None + # menu(s) associated with the panel + self._menu = {} self._menuModel = None self._menuName = None @@ -134,9 +134,12 @@ def GetMenu(self): """ menu = None if self._menuModel is not None: - menuClass = GMenuItem if self._docked else GMenu - menu = menuClass( - parent=self.parent, model=self._menuModel, class_handler=self - ) + 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 From 83a4a4c753617690ba9a59d1979b01854a485ca2 Mon Sep 17 00:00:00 2001 From: Martin Landa Date: Fri, 22 Mar 2024 23:37:07 +0100 Subject: [PATCH 44/51] mapDispDocking renamed to docking --- gui/wxpython/gmodeler/toolbars.py | 4 ++-- gui/wxpython/gui_core/toolbars.py | 2 +- gui/wxpython/mapdisp/toolbars.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/gui/wxpython/gmodeler/toolbars.py b/gui/wxpython/gmodeler/toolbars.py index 1c809ba2a8e..80404c61a22 100644 --- a/gui/wxpython/gmodeler/toolbars.py +++ b/gui/wxpython/gmodeler/toolbars.py @@ -186,8 +186,8 @@ def _toolbarData(self): if self.parent.IsDockable(): data += ( ( - ("mapDispDocking", BaseIcons["mapDispDocking"].label), - BaseIcons["mapDispDocking"], + ("docking", BaseIcons["docking"].label), + BaseIcons["docking"], self.parent.OnDockUndock, wx.ITEM_CHECK, ), 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/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, ), From 78ad9a7be20a53a0a57083e3747b441fd5132f5f Mon Sep 17 00:00:00 2001 From: Martin Landa Date: Fri, 22 Mar 2024 23:43:21 +0100 Subject: [PATCH 45/51] fix 'MapPanel' object has no attribute '_saved_output_img_size' --- gui/wxpython/mapdisp/frame.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gui/wxpython/mapdisp/frame.py b/gui/wxpython/mapdisp/frame.py index a5b71714135..6e10937ec6d 100644 --- a/gui/wxpython/mapdisp/frame.py +++ b/gui/wxpython/mapdisp/frame.py @@ -114,6 +114,9 @@ def __init__( # used for VDigit toolbar and window and GLWindow self.tree = tree + # Saved Map Display output img size + self._saved_output_img_size = None + # Emitted when starting (switching to) 3D mode. # Parameter firstTime specifies if 3D was already activated. self.starting3dMode = Signal("MapPanel.starting3dMode") From d3874326aae0ae4bceffcd983aa5a3ec755e4c05 Mon Sep 17 00:00:00 2001 From: Martin Landa Date: Wed, 3 Apr 2024 08:14:26 +0200 Subject: [PATCH 46/51] ignore security alert - consider using safer ast.literal_eval --- gui/wxpython/gui_core/menu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/wxpython/gui_core/menu.py b/gui/wxpython/gui_core/menu.py index 5470635672e..60f041e51f7 100644 --- a/gui/wxpython/gui_core/menu.py +++ b/gui/wxpython/gui_core/menu.py @@ -121,7 +121,7 @@ def _createMenuItem( ): menuItem.Enable(False) - rhandler = eval("self.class_handler." + handler) + rhandler = eval("self.class_handler." + handler) # nosec B602 self.parent.Bind(wx.EVT_MENU, rhandler, menuItem) def GetData(self): From 59be93ed57a4066fe89ed7028c137775ddaa9549 Mon Sep 17 00:00:00 2001 From: Martin Landa Date: Wed, 3 Apr 2024 08:26:18 +0200 Subject: [PATCH 47/51] nosec: fix rule id --- gui/wxpython/gui_core/menu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/wxpython/gui_core/menu.py b/gui/wxpython/gui_core/menu.py index 60f041e51f7..55ba0d40d48 100644 --- a/gui/wxpython/gui_core/menu.py +++ b/gui/wxpython/gui_core/menu.py @@ -121,7 +121,7 @@ def _createMenuItem( ): menuItem.Enable(False) - rhandler = eval("self.class_handler." + handler) # nosec B602 + rhandler = eval("self.class_handler." + handler) # nosec B307 self.parent.Bind(wx.EVT_MENU, rhandler, menuItem) def GetData(self): From 846a3837c066adb057911cb77eadf58e0e6b2c8e Mon Sep 17 00:00:00 2001 From: Martin Landa Date: Wed, 3 Apr 2024 14:05:56 +0200 Subject: [PATCH 48/51] fix missing imports (TextEntryDialogs) --- gui/wxpython/gmodeler/canvas.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gui/wxpython/gmodeler/canvas.py b/gui/wxpython/gmodeler/canvas.py index d10c8b4e0ee..580e9f0d730 100644 --- a/gui/wxpython/gmodeler/canvas.py +++ b/gui/wxpython/gmodeler/canvas.py @@ -19,6 +19,9 @@ import wx from wx.lib import ogl +from gui_core.dialogs import TextEntryDialog as CustomTextEntryDialog +from gui_core.wrap import TextEntryDialog as wxTextEntryDialog + from gmodeler.model import * from gmodeler.dialogs import * From cd788741f54360554a96a5a3e3867ff000f3704b Mon Sep 17 00:00:00 2001 From: Martin Landa Date: Fri, 5 Apr 2024 12:42:51 +0200 Subject: [PATCH 49/51] address issues reported by flake8 --- gui/wxpython/gmodeler/canvas.py | 28 +++++++++++++++++++++------- gui/wxpython/gmodeler/dialogs.py | 3 +-- gui/wxpython/gmodeler/model.py | 10 ++++------ gui/wxpython/gmodeler/panels.py | 26 ++++++++++++++++++++++---- gui/wxpython/gmodeler/toolbars.py | 4 ---- 5 files changed, 48 insertions(+), 23 deletions(-) diff --git a/gui/wxpython/gmodeler/canvas.py b/gui/wxpython/gmodeler/canvas.py index 580e9f0d730..00a71fadc7c 100644 --- a/gui/wxpython/gmodeler/canvas.py +++ b/gui/wxpython/gmodeler/canvas.py @@ -20,10 +20,25 @@ from wx.lib import ogl from gui_core.dialogs import TextEntryDialog as CustomTextEntryDialog -from gui_core.wrap import TextEntryDialog as wxTextEntryDialog - -from gmodeler.model import * -from gmodeler.dialogs import * +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): @@ -115,10 +130,10 @@ def __init__(self, log, frame): 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 = shape.GetCanvas() + # dc = wx.ClientDC(canvas) # canvas.PrepareDC(dc) if hasattr(self.frame, "defineRelation"): @@ -424,7 +439,6 @@ def _onSelectShape(self, shape, append=False): if shape.Selected(): shape.Select(False, dc) else: - redraw = False shapeList = canvas.GetDiagram().GetShapeList() toUnselect = list() 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/model.py b/gui/wxpython/gmodeler/model.py index ed50eb69c28..6b4e84a2681 100644 --- a/gui/wxpython/gmodeler/model.py +++ b/gui/wxpython/gmodeler/model.py @@ -315,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)) @@ -1145,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() @@ -1999,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 @@ -2137,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) @@ -2145,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 index c7d6c64359c..e611951ba58 100644 --- a/gui/wxpython/gmodeler/panels.py +++ b/gui/wxpython/gmodeler/panels.py @@ -65,17 +65,35 @@ ) from main_window.page import MainPageBase from gmodeler.giface import GraphicalModelerGrassInterface -from gmodeler.model import * -from gmodeler.dialogs import * +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 -wxModelDone, EVT_MODEL_DONE = NewEvent() - 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__( diff --git a/gui/wxpython/gmodeler/toolbars.py b/gui/wxpython/gmodeler/toolbars.py index 80404c61a22..ca6a75b4804 100644 --- a/gui/wxpython/gmodeler/toolbars.py +++ b/gui/wxpython/gmodeler/toolbars.py @@ -40,10 +40,6 @@ def __init__(self, parent): def _toolbarData(self): """Toolbar data""" - # dockable window has no menu, so shortcuts doesn't work when - # window is dockable - show_shortcuts = not self.parent.IsDockable() - icons = { "new": MetaIcon( img="create", From 174335e9806219edb9ed434c4e815c6c244e2931 Mon Sep 17 00:00:00 2001 From: Martin Landa Date: Fri, 5 Apr 2024 12:52:58 +0200 Subject: [PATCH 50/51] variables are expected to be dict --- gui/wxpython/gmodeler/panels.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/wxpython/gmodeler/panels.py b/gui/wxpython/gmodeler/panels.py index e611951ba58..1e650ffaa1c 100644 --- a/gui/wxpython/gmodeler/panels.py +++ b/gui/wxpython/gmodeler/panels.py @@ -1477,7 +1477,7 @@ def Update(self): def Reset(self): """Remove all variables""" self.list.DeleteAllItems() - self.parent.GetModel().SetVariables([]) + self.parent.GetModel().SetVariables({}) class ItemPanel(wx.Panel): From c1c010f42adb75f613631794f6326c7b7cf5d7db Mon Sep 17 00:00:00 2001 From: Martin Landa Date: Fri, 5 Apr 2024 20:41:44 +0200 Subject: [PATCH 51/51] update .flake8 --- .flake8 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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