frame.py 71 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176
  1. """
  2. @package gmodeler.frame
  3. @brief wxGUI Graphical Modeler for creating, editing, and managing models
  4. Classes:
  5. - frame::ModelFrame
  6. - frame::ModelCanvas
  7. - frame::ModelEvtHandler
  8. - frame::VariablePanel
  9. - frame::ItemPanel
  10. - frame::PythonPanel
  11. (C) 2010-2018 by the GRASS Development Team
  12. This program is free software under the GNU General Public License
  13. (>=v2). Read the file COPYING that comes with GRASS for details.
  14. @author Martin Landa <landa.martin gmail.com>
  15. @author Python parameterization Ondrej Pesek <pesej.ondrek gmail.com>
  16. """
  17. import os
  18. import sys
  19. import time
  20. import stat
  21. import tempfile
  22. import random
  23. import six
  24. import wx
  25. from wx.lib import ogl
  26. from core import globalvar
  27. if globalvar.wxPythonPhoenix:
  28. try:
  29. import agw.flatnotebook as FN
  30. except ImportError: # if it's not there locally, try the wxPython lib.
  31. import wx.lib.agw.flatnotebook as FN
  32. else:
  33. import wx.lib.flatnotebook as FN
  34. from wx.lib.newevent import NewEvent
  35. from gui_core.widgets import GNotebook
  36. from core.gconsole import GConsole, EVT_CMD_RUN, EVT_CMD_DONE, EVT_CMD_PREPARE
  37. from gui_core.goutput import GConsoleWindow
  38. from core.debug import Debug
  39. from core.gcmd import GMessage, GException, GWarning, GError
  40. from gui_core.dialogs import GetImageHandlers
  41. from gui_core.dialogs import TextEntryDialog as CustomTextEntryDialog
  42. from gui_core.ghelp import ShowAboutDialog
  43. from core.settings import UserSettings
  44. from gui_core.menu import Menu as Menubar
  45. from gmodeler.menudata import ModelerMenuData
  46. from gui_core.forms import GUI
  47. from gmodeler.preferences import PreferencesDialog, PropertiesDialog
  48. from gmodeler.toolbars import ModelerToolbar
  49. from core.giface import Notification
  50. from gui_core.pystc import PyStc, SetDarkMode
  51. from gmodeler.giface import GraphicalModelerGrassInterface
  52. from gmodeler.model import *
  53. from gmodeler.dialogs import *
  54. from gui_core.wrap import (
  55. Button,
  56. EmptyBitmap,
  57. ImageFromBitmap,
  58. Menu,
  59. NewId,
  60. StaticBox,
  61. StaticText,
  62. StockCursor,
  63. TextCtrl,
  64. IsDark,
  65. )
  66. from gui_core.wrap import TextEntryDialog as wxTextEntryDialog
  67. wxModelDone, EVT_MODEL_DONE = NewEvent()
  68. from grass.script.utils import try_remove
  69. from grass.script import core as grass
  70. class ModelFrame(wx.Frame):
  71. def __init__(
  72. self, parent, giface, id=wx.ID_ANY, title=_("Graphical Modeler"), **kwargs
  73. ):
  74. """Graphical modeler main window
  75. :param parent: parent window
  76. :param id: window id
  77. :param title: window title
  78. :param kwargs: wx.Frames' arguments
  79. """
  80. self.parent = parent
  81. self._giface = giface
  82. self.searchDialog = None # module search dialog
  83. self.baseTitle = title
  84. self.modelFile = None # loaded model
  85. self.start_time = None
  86. self.modelChanged = False
  87. self.randomness = 40 # random layout
  88. self.cursors = {
  89. "default": StockCursor(wx.CURSOR_ARROW),
  90. "cross": StockCursor(wx.CURSOR_CROSS),
  91. }
  92. wx.Frame.__init__(self, parent=parent, id=id, title=title, **kwargs)
  93. self.SetName("Modeler")
  94. self.SetIcon(
  95. wx.Icon(os.path.join(globalvar.ICONDIR, "grass.ico"), wx.BITMAP_TYPE_ICO)
  96. )
  97. self.menubar = Menubar(
  98. parent=self, model=ModelerMenuData().GetModel(separators=True)
  99. )
  100. self.SetMenuBar(self.menubar)
  101. self.toolbar = ModelerToolbar(parent=self)
  102. # workaround for http://trac.wxwidgets.org/ticket/13888
  103. if sys.platform != "darwin":
  104. self.SetToolBar(self.toolbar)
  105. self.statusbar = self.CreateStatusBar(number=1)
  106. self.notebook = GNotebook(parent=self, style=globalvar.FNPageDStyle)
  107. self.canvas = ModelCanvas(self)
  108. self.canvas.SetBackgroundColour(
  109. wx.SystemSettings().GetColour(wx.SYS_COLOUR_WINDOW)
  110. )
  111. self.canvas.SetCursor(self.cursors["default"])
  112. self.model = Model(self.canvas)
  113. self.variablePanel = VariablePanel(parent=self)
  114. self.itemPanel = ItemPanel(parent=self)
  115. self.pythonPanel = PythonPanel(parent=self)
  116. self._gconsole = GConsole(guiparent=self)
  117. self.goutput = GConsoleWindow(
  118. parent=self, giface=giface, gconsole=self._gconsole
  119. )
  120. self.goutput.showNotification.connect(
  121. lambda message: self.SetStatusText(message)
  122. )
  123. # here events are binded twice
  124. self._gconsole.Bind(
  125. EVT_CMD_RUN,
  126. lambda event: self._switchPageHandler(
  127. event=event, notification=Notification.MAKE_VISIBLE
  128. ),
  129. )
  130. self._gconsole.Bind(
  131. EVT_CMD_DONE,
  132. lambda event: self._switchPageHandler(
  133. event=event, notification=Notification.RAISE_WINDOW
  134. ),
  135. )
  136. self.Bind(EVT_CMD_RUN, self.OnCmdRun)
  137. # rewrite default method to avoid hiding progress bar
  138. self._gconsole.Bind(EVT_CMD_DONE, self.OnCmdDone)
  139. self.Bind(EVT_CMD_PREPARE, self.OnCmdPrepare)
  140. self.Bind(EVT_MODEL_DONE, self.OnModelDone)
  141. self.notebook.AddPage(page=self.canvas, text=_("Model"), name="model")
  142. self.notebook.AddPage(page=self.itemPanel, text=_("Items"), name="items")
  143. self.notebook.AddPage(
  144. page=self.variablePanel, text=_("Variables"), name="variables"
  145. )
  146. self.notebook.AddPage(
  147. page=self.pythonPanel, text=_("Python editor"), name="python"
  148. )
  149. self.notebook.AddPage(
  150. page=self.goutput, text=_("Command output"), name="output"
  151. )
  152. wx.CallAfter(self.notebook.SetSelectionByName, "model")
  153. wx.CallAfter(self.ModelChanged, False)
  154. self.Bind(wx.EVT_CLOSE, self.OnCloseWindow)
  155. self.Bind(wx.EVT_SIZE, self.OnSize)
  156. self.notebook.Bind(FN.EVT_FLATNOTEBOOK_PAGE_CHANGED, self.OnPageChanged)
  157. self._layout()
  158. self.SetMinSize((640, 300))
  159. self.SetSize((800, 600))
  160. # fix goutput's pane size
  161. if self.goutput:
  162. self.goutput.SetSashPosition(int(self.GetSize()[1] * 0.75))
  163. def _layout(self):
  164. """Do layout"""
  165. sizer = wx.BoxSizer(wx.VERTICAL)
  166. sizer.Add(self.notebook, proportion=1, flag=wx.EXPAND)
  167. self.SetAutoLayout(True)
  168. self.SetSizer(sizer)
  169. sizer.Fit(self)
  170. self.Layout()
  171. def _addEvent(self, item):
  172. """Add event to item"""
  173. evthandler = ModelEvtHandler(self.statusbar, self)
  174. evthandler.SetShape(item)
  175. evthandler.SetPreviousHandler(item.GetEventHandler())
  176. item.SetEventHandler(evthandler)
  177. def _randomShift(self):
  178. """Returns random value to shift layout"""
  179. return random.randint(-self.randomness, self.randomness)
  180. def GetCanvas(self):
  181. """Get canvas"""
  182. return self.canvas
  183. def GetModel(self):
  184. """Get model"""
  185. return self.model
  186. def ModelChanged(self, changed=True):
  187. """Update window title"""
  188. self.modelChanged = changed
  189. if self.modelFile:
  190. if self.modelChanged:
  191. self.SetTitle(
  192. self.baseTitle + " - " + os.path.basename(self.modelFile) + "*"
  193. )
  194. else:
  195. self.SetTitle(self.baseTitle + " - " + os.path.basename(self.modelFile))
  196. else:
  197. self.SetTitle(self.baseTitle)
  198. def OnPageChanged(self, event):
  199. """Page in notebook changed"""
  200. page = event.GetSelection()
  201. if page == self.notebook.GetPageIndexByName("python"):
  202. if self.pythonPanel.IsEmpty():
  203. self.pythonPanel.RefreshScript()
  204. if self.pythonPanel.IsModified():
  205. self.SetStatusText(_("Python script contains local modifications"), 0)
  206. else:
  207. self.SetStatusText(_("Python script is up-to-date"), 0)
  208. elif page == self.notebook.GetPageIndexByName("items"):
  209. self.itemPanel.Update()
  210. event.Skip()
  211. def OnVariables(self, event):
  212. """Switch to variables page"""
  213. self.notebook.SetSelectionByName("variables")
  214. def OnRemoveItem(self, event):
  215. """Remove shape"""
  216. self.GetCanvas().RemoveSelected()
  217. def OnCanvasRefresh(self, event):
  218. """Refresh canvas"""
  219. self.SetStatusText(_("Redrawing model..."), 0)
  220. self.GetCanvas().Refresh()
  221. self.SetStatusText("", 0)
  222. def OnCmdRun(self, event):
  223. """Run command"""
  224. try:
  225. action = self.GetModel().GetItems()[event.pid]
  226. if hasattr(action, "task"):
  227. action.Update(running=True)
  228. except IndexError:
  229. pass
  230. def OnCmdPrepare(self, event):
  231. """Prepare for running command"""
  232. if not event.userData:
  233. return
  234. event.onPrepare(item=event.userData["item"], params=event.userData["params"])
  235. def OnCmdDone(self, event):
  236. """Command done (or aborted)"""
  237. def time_elapsed(etime):
  238. try:
  239. ctime = time.time() - etime
  240. if ctime < 60:
  241. stime = _("%d sec") % int(ctime)
  242. else:
  243. mtime = int(ctime / 60)
  244. stime = _("%(min)d min %(sec)d sec") % {
  245. "min": mtime,
  246. "sec": int(ctime - (mtime * 60)),
  247. }
  248. except KeyError:
  249. # stopped deamon
  250. stime = _("unknown")
  251. return stime
  252. self.goutput.GetProgressBar().SetValue(0)
  253. self.goutput.WriteCmdLog(
  254. "({}) {} ({})".format(
  255. str(time.ctime()), _("Command finished"), time_elapsed(event.time)
  256. ),
  257. notification=event.notification,
  258. )
  259. try:
  260. action = self.GetModel().GetItems()[event.pid]
  261. if hasattr(action, "task"):
  262. action.Update(running=True)
  263. if event.pid == self._gconsole.cmdThread.GetId() - 1 and self.start_time:
  264. self.goutput.WriteCmdLog(
  265. "({}) {} ({})".format(
  266. str(time.ctime()),
  267. _("Model computation finished"),
  268. time_elapsed(self.start_time),
  269. ),
  270. notification=event.notification,
  271. )
  272. event = wxModelDone()
  273. wx.PostEvent(self, event)
  274. except IndexError:
  275. pass
  276. def OnCloseWindow(self, event):
  277. """Close window"""
  278. if self.modelChanged and UserSettings.Get(
  279. group="manager", key="askOnQuit", subkey="enabled"
  280. ):
  281. if self.modelFile:
  282. message = _("Do you want to save changes in the model?")
  283. else:
  284. message = _(
  285. "Do you want to store current model settings " "to model file?"
  286. )
  287. # ask user to save current settings
  288. dlg = wx.MessageDialog(
  289. self,
  290. message=message,
  291. caption=_("Quit Graphical Modeler"),
  292. style=wx.YES_NO
  293. | wx.YES_DEFAULT
  294. | wx.CANCEL
  295. | wx.ICON_QUESTION
  296. | wx.CENTRE,
  297. )
  298. ret = dlg.ShowModal()
  299. if ret == wx.ID_YES:
  300. if not self.modelFile:
  301. self.OnWorkspaceSaveAs()
  302. else:
  303. self.WriteModelFile(self.modelFile)
  304. elif ret == wx.ID_CANCEL:
  305. dlg.Destroy()
  306. return
  307. dlg.Destroy()
  308. self.Destroy()
  309. def OnSize(self, event):
  310. """Window resized, save to the model"""
  311. self.ModelChanged()
  312. event.Skip()
  313. def OnPreferences(self, event):
  314. """Open preferences dialog"""
  315. dlg = PreferencesDialog(parent=self, giface=self._giface)
  316. dlg.CenterOnParent()
  317. dlg.Show()
  318. self.canvas.Refresh()
  319. def OnHelp(self, event):
  320. """Show help"""
  321. self._giface.Help(entry="wxGUI.gmodeler")
  322. def OnModelProperties(self, event):
  323. """Model properties dialog"""
  324. dlg = PropertiesDialog(parent=self)
  325. dlg.CentreOnParent()
  326. properties = self.model.GetProperties()
  327. dlg.Init(properties)
  328. if dlg.ShowModal() == wx.ID_OK:
  329. self.ModelChanged()
  330. for key, value in six.iteritems(dlg.GetValues()):
  331. properties[key] = value
  332. for action in self.model.GetItems(objType=ModelAction):
  333. action.GetTask().set_flag("overwrite", properties["overwrite"])
  334. dlg.Destroy()
  335. def _deleteIntermediateData(self):
  336. """Delete intermediate data"""
  337. rast, vect, rast3d, msg = self.model.GetIntermediateData()
  338. if rast:
  339. self._gconsole.RunCmd(
  340. ["g.remove", "-f", "type=raster", "name=%s" % ",".join(rast)]
  341. )
  342. if rast3d:
  343. self._gconsole.RunCmd(
  344. ["g.remove", "-f", "type=raster_3d", "name=%s" % ",".join(rast3d)]
  345. )
  346. if vect:
  347. self._gconsole.RunCmd(
  348. ["g.remove", "-f", "type=vector", "name=%s" % ",".join(vect)]
  349. )
  350. self.SetStatusText(
  351. _("%d intermediate maps deleted from current mapset")
  352. % int(len(rast) + len(rast3d) + len(vect))
  353. )
  354. def OnDeleteData(self, event):
  355. """Delete intermediate data"""
  356. rast, vect, rast3d, msg = self.model.GetIntermediateData()
  357. if not rast and not vect and not rast3d:
  358. GMessage(parent=self, message=_("No intermediate data to delete."))
  359. return
  360. dlg = wx.MessageDialog(
  361. parent=self,
  362. message=_("Do you want to permanently delete data?%s" % msg),
  363. caption=_("Delete intermediate data?"),
  364. style=wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION,
  365. )
  366. ret = dlg.ShowModal()
  367. dlg.Destroy()
  368. if ret == wx.ID_YES:
  369. self._deleteIntermediateData()
  370. def OnModelNew(self, event):
  371. """Create new model"""
  372. Debug.msg(4, "ModelFrame.OnModelNew():")
  373. # ask user to save current model
  374. if self.modelFile and self.modelChanged:
  375. self.OnModelSave()
  376. elif self.modelFile is None and (
  377. self.model.GetNumItems() > 0 or len(self.model.GetData()) > 0
  378. ):
  379. dlg = wx.MessageDialog(
  380. self,
  381. message=_(
  382. "Current model is not empty. "
  383. "Do you want to store current settings "
  384. "to model file?"
  385. ),
  386. caption=_("Create new model?"),
  387. style=wx.YES_NO | wx.YES_DEFAULT | wx.CANCEL | wx.ICON_QUESTION,
  388. )
  389. ret = dlg.ShowModal()
  390. if ret == wx.ID_YES:
  391. self.OnModelSaveAs()
  392. elif ret == wx.ID_CANCEL:
  393. dlg.Destroy()
  394. return
  395. dlg.Destroy()
  396. # delete all items
  397. self.canvas.GetDiagram().DeleteAllShapes()
  398. self.model.Reset()
  399. self.canvas.Refresh()
  400. self.itemPanel.Update()
  401. self.variablePanel.Reset()
  402. # no model file loaded
  403. self.modelFile = None
  404. self.modelChanged = False
  405. self.SetTitle(self.baseTitle)
  406. def GetModelFile(self, ext=True):
  407. """Get model file
  408. :param bool ext: False to avoid extension
  409. """
  410. if not self.modelFile:
  411. return ""
  412. if ext:
  413. return self.modelFile
  414. return os.path.splitext(self.modelFile)[0]
  415. def OnModelOpen(self, event):
  416. """Load model from file"""
  417. filename = ""
  418. dlg = wx.FileDialog(
  419. parent=self,
  420. message=_("Choose model file"),
  421. defaultDir=os.getcwd(),
  422. wildcard=_("GRASS Model File (*.gxm)|*.gxm"),
  423. )
  424. if dlg.ShowModal() == wx.ID_OK:
  425. filename = dlg.GetPath()
  426. if not filename:
  427. return
  428. Debug.msg(4, "ModelFrame.OnModelOpen(): filename=%s" % filename)
  429. # close current model
  430. self.OnModelClose()
  431. self.LoadModelFile(filename)
  432. self.modelFile = filename
  433. self.SetTitle(self.baseTitle + " - " + os.path.basename(self.modelFile))
  434. self.SetStatusText(
  435. _("%(items)d items (%(actions)d actions) loaded into model")
  436. % {
  437. "items": self.model.GetNumItems(),
  438. "actions": self.model.GetNumItems(actionOnly=True),
  439. },
  440. 0,
  441. )
  442. def OnModelSave(self, event=None):
  443. """Save model to file"""
  444. if self.modelFile and self.modelChanged:
  445. dlg = wx.MessageDialog(
  446. self,
  447. message=_(
  448. "Model file <%s> already exists. "
  449. "Do you want to overwrite this file?"
  450. )
  451. % self.modelFile,
  452. caption=_("Save model"),
  453. style=wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION,
  454. )
  455. if dlg.ShowModal() == wx.ID_NO:
  456. dlg.Destroy()
  457. else:
  458. Debug.msg(4, "ModelFrame.OnModelSave(): filename=%s" % self.modelFile)
  459. self.WriteModelFile(self.modelFile)
  460. self.SetStatusText(_("File <%s> saved") % self.modelFile, 0)
  461. self.SetTitle(self.baseTitle + " - " + os.path.basename(self.modelFile))
  462. elif not self.modelFile:
  463. self.OnModelSaveAs(None)
  464. def OnModelSaveAs(self, event):
  465. """Create model to file as"""
  466. filename = ""
  467. dlg = wx.FileDialog(
  468. parent=self,
  469. message=_("Choose file to save current model"),
  470. defaultDir=os.getcwd(),
  471. wildcard=_("GRASS Model File (*.gxm)|*.gxm"),
  472. style=wx.FD_SAVE,
  473. )
  474. if dlg.ShowModal() == wx.ID_OK:
  475. filename = dlg.GetPath()
  476. if not filename:
  477. return
  478. # check for extension
  479. if filename[-4:] != ".gxm":
  480. filename += ".gxm"
  481. if os.path.exists(filename):
  482. dlg = wx.MessageDialog(
  483. parent=self,
  484. message=_(
  485. "Model file <%s> already exists. "
  486. "Do you want to overwrite this file?"
  487. )
  488. % filename,
  489. caption=_("File already exists"),
  490. style=wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION,
  491. )
  492. if dlg.ShowModal() != wx.ID_YES:
  493. dlg.Destroy()
  494. return
  495. Debug.msg(4, "GMFrame.OnModelSaveAs(): filename=%s" % filename)
  496. self.WriteModelFile(filename)
  497. self.modelFile = filename
  498. self.SetTitle(self.baseTitle + " - " + os.path.basename(self.modelFile))
  499. self.SetStatusText(_("File <%s> saved") % self.modelFile, 0)
  500. def OnModelClose(self, event=None):
  501. """Close model file"""
  502. Debug.msg(4, "ModelFrame.OnModelClose(): file=%s" % self.modelFile)
  503. # ask user to save current model
  504. if self.modelFile and self.modelChanged:
  505. self.OnModelSave()
  506. elif self.modelFile is None and (
  507. self.model.GetNumItems() > 0 or len(self.model.GetData()) > 0
  508. ):
  509. dlg = wx.MessageDialog(
  510. self,
  511. message=_(
  512. "Current model is not empty. "
  513. "Do you want to store current settings "
  514. "to model file?"
  515. ),
  516. caption=_("Create new model?"),
  517. style=wx.YES_NO | wx.YES_DEFAULT | wx.CANCEL | wx.ICON_QUESTION,
  518. )
  519. ret = dlg.ShowModal()
  520. if ret == wx.ID_YES:
  521. self.OnModelSaveAs()
  522. elif ret == wx.ID_CANCEL:
  523. dlg.Destroy()
  524. return
  525. dlg.Destroy()
  526. self.modelFile = None
  527. self.SetTitle(self.baseTitle)
  528. self.canvas.GetDiagram().DeleteAllShapes()
  529. self.model.Reset()
  530. self.canvas.Refresh()
  531. def OnRunModel(self, event):
  532. """Run entire model"""
  533. self.start_time = time.time()
  534. self.model.Run(self._gconsole, self.OnModelDone, parent=self)
  535. def OnModelDone(self, event):
  536. """Computation finished"""
  537. self.SetStatusText("", 0)
  538. # restore original files
  539. if hasattr(self.model, "fileInput"):
  540. for finput in self.model.fileInput:
  541. data = self.model.fileInput[finput]
  542. if not data:
  543. continue
  544. fd = open(finput, "w")
  545. try:
  546. fd.write(data)
  547. finally:
  548. fd.close()
  549. del self.model.fileInput
  550. # delete intermediate data
  551. self._deleteIntermediateData()
  552. # display data if required
  553. for data in self.model.GetData():
  554. if not data.HasDisplay():
  555. continue
  556. # remove existing map layers first
  557. layers = self._giface.GetLayerList().GetLayersByName(data.GetValue())
  558. if layers:
  559. for layer in layers:
  560. self._giface.GetLayerList().DeleteLayer(layer)
  561. # add new map layer
  562. self._giface.GetLayerList().AddLayer(
  563. ltype=data.GetPrompt(),
  564. name=data.GetValue(),
  565. checked=True,
  566. cmd=data.GetDisplayCmd(),
  567. )
  568. def OnValidateModel(self, event, showMsg=True):
  569. """Validate entire model"""
  570. if self.model.GetNumItems() < 1:
  571. GMessage(parent=self, message=_("Model is empty. Nothing to validate."))
  572. return
  573. self.SetStatusText(_("Validating model..."), 0)
  574. errList = self.model.Validate()
  575. self.SetStatusText("", 0)
  576. if errList:
  577. GWarning(
  578. parent=self, message=_("Model is not valid.\n\n%s") % "\n".join(errList)
  579. )
  580. else:
  581. GMessage(parent=self, message=_("Model is valid."))
  582. def OnExportImage(self, event):
  583. """Export model to image (default image)"""
  584. xminImg = 0
  585. xmaxImg = 0
  586. yminImg = 0
  587. ymaxImg = 0
  588. # get current size of canvas
  589. for shape in self.canvas.GetDiagram().GetShapeList():
  590. w, h = shape.GetBoundingBoxMax()
  591. x = shape.GetX()
  592. y = shape.GetY()
  593. xmin = x - w / 2
  594. xmax = x + w / 2
  595. ymin = y - h / 2
  596. ymax = y + h / 2
  597. if xmin < xminImg:
  598. xminImg = xmin
  599. if xmax > xmaxImg:
  600. xmaxImg = xmax
  601. if ymin < yminImg:
  602. yminImg = ymin
  603. if ymax > ymaxImg:
  604. ymaxImg = ymax
  605. size = wx.Size(int(xmaxImg - xminImg) + 50, int(ymaxImg - yminImg) + 50)
  606. bitmap = EmptyBitmap(width=size.width, height=size.height)
  607. filetype, ltype = GetImageHandlers(ImageFromBitmap(bitmap))
  608. dlg = wx.FileDialog(
  609. parent=self,
  610. message=_(
  611. "Choose a file name to save the image (no need to add extension)"
  612. ),
  613. defaultDir="",
  614. defaultFile="",
  615. wildcard=filetype,
  616. style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT,
  617. )
  618. if dlg.ShowModal() == wx.ID_OK:
  619. path = dlg.GetPath()
  620. if not path:
  621. dlg.Destroy()
  622. return
  623. base, ext = os.path.splitext(path)
  624. fileType = ltype[dlg.GetFilterIndex()]["type"]
  625. extType = ltype[dlg.GetFilterIndex()]["ext"]
  626. if ext != extType:
  627. path = base + "." + extType
  628. dc = wx.MemoryDC(bitmap)
  629. dc.SetBackground(wx.WHITE_BRUSH)
  630. dc.SetBackgroundMode(wx.SOLID)
  631. self.canvas.GetDiagram().Clear(dc)
  632. self.canvas.GetDiagram().Redraw(dc)
  633. bitmap.SaveFile(path, fileType)
  634. self.SetStatusText(_("Model exported to <%s>") % path)
  635. dlg.Destroy()
  636. def OnExportPython(self, event=None, text=None):
  637. """Export model to Python script"""
  638. filename = self.pythonPanel.SaveAs(force=True)
  639. self.SetStatusText(_("Model exported to <%s>") % filename)
  640. def OnDefineRelation(self, event):
  641. """Define relation between data and action items"""
  642. self.canvas.SetCursor(self.cursors["cross"])
  643. self.defineRelation = {"from": None, "to": None}
  644. def OnDefineLoop(self, event):
  645. """Define new loop in the model
  646. .. todo::
  647. move to ModelCanvas?
  648. """
  649. self.ModelChanged()
  650. width, height = self.canvas.GetSize()
  651. loop = ModelLoop(
  652. self, x=width / 2, y=height / 2, id=self.model.GetNumItems() + 1
  653. )
  654. self.canvas.diagram.AddShape(loop)
  655. loop.Show(True)
  656. self._addEvent(loop)
  657. self.model.AddItem(loop)
  658. self.canvas.Refresh()
  659. def OnDefineCondition(self, event):
  660. """Define new condition in the model
  661. .. todo::
  662. move to ModelCanvas?
  663. """
  664. self.ModelChanged()
  665. width, height = self.canvas.GetSize()
  666. cond = ModelCondition(
  667. self, x=width / 2, y=height / 2, id=self.model.GetNumItems() + 1
  668. )
  669. self.canvas.diagram.AddShape(cond)
  670. cond.Show(True)
  671. self._addEvent(cond)
  672. self.model.AddItem(cond)
  673. self.canvas.Refresh()
  674. def OnAddAction(self, event):
  675. """Add action to model"""
  676. if self.searchDialog is None:
  677. self.searchDialog = ModelSearchDialog(parent=self, giface=self._giface)
  678. self.searchDialog.CentreOnParent()
  679. else:
  680. self.searchDialog.Reset()
  681. if self.searchDialog.ShowModal() == wx.ID_CANCEL:
  682. self.searchDialog.Hide()
  683. return
  684. cmd = self.searchDialog.GetCmd()
  685. self.searchDialog.Hide()
  686. self.ModelChanged()
  687. # add action to canvas
  688. x, y = self.canvas.GetNewShapePos()
  689. label, comment = self.searchDialog.GetLabel()
  690. action = ModelAction(
  691. self.model,
  692. cmd=cmd,
  693. x=x + self._randomShift(),
  694. y=y + self._randomShift(),
  695. id=self.model.GetNextId(),
  696. label=label,
  697. comment=comment,
  698. )
  699. overwrite = self.model.GetProperties().get("overwrite", None)
  700. if overwrite is not None:
  701. action.GetTask().set_flag("overwrite", overwrite)
  702. self.canvas.diagram.AddShape(action)
  703. action.Show(True)
  704. self._addEvent(action)
  705. self.model.AddItem(action)
  706. self.itemPanel.Update()
  707. self.canvas.Refresh()
  708. time.sleep(0.1)
  709. # show properties dialog
  710. win = action.GetPropDialog()
  711. if not win:
  712. cmdLength = len(action.GetLog(string=False))
  713. if cmdLength > 1 and action.IsValid():
  714. self.GetOptData(
  715. dcmd=action.GetLog(string=False),
  716. layer=action,
  717. params=action.GetParams(),
  718. propwin=None,
  719. )
  720. else:
  721. gmodule = GUI(
  722. parent=self,
  723. show=True,
  724. giface=GraphicalModelerGrassInterface(self.model),
  725. )
  726. gmodule.ParseCommand(
  727. action.GetLog(string=False),
  728. completed=(self.GetOptData, action, action.GetParams()),
  729. )
  730. elif win and not win.IsShown():
  731. win.Show()
  732. if win:
  733. win.Raise()
  734. def OnAddData(self, event):
  735. """Add data item to model"""
  736. # add action to canvas
  737. width, height = self.canvas.GetSize()
  738. data = ModelData(
  739. self, x=width / 2 + self._randomShift(), y=height / 2 + self._randomShift()
  740. )
  741. dlg = ModelDataDialog(parent=self, shape=data)
  742. data.SetPropDialog(dlg)
  743. dlg.CentreOnParent()
  744. ret = dlg.ShowModal()
  745. dlg.Destroy()
  746. if ret != wx.ID_OK:
  747. return
  748. data.Update()
  749. self.canvas.diagram.AddShape(data)
  750. data.Show(True)
  751. self.ModelChanged()
  752. self._addEvent(data)
  753. self.model.AddItem(data)
  754. self.canvas.Refresh()
  755. def OnAddComment(self, event):
  756. """Add comment to the model"""
  757. dlg = CustomTextEntryDialog(
  758. parent=self,
  759. message=_("Comment:"),
  760. caption=_("Add comment"),
  761. textStyle=wx.TE_MULTILINE,
  762. textSize=(300, 75),
  763. )
  764. if dlg.ShowModal() == wx.ID_OK:
  765. comment = dlg.GetValue()
  766. if not comment:
  767. GError(_("Empty comment. Nothing to add to the model."), parent=self)
  768. else:
  769. x, y = self.canvas.GetNewShapePos()
  770. commentObj = ModelComment(
  771. self.model,
  772. x=x + self._randomShift(),
  773. y=y + self._randomShift(),
  774. id=self.model.GetNextId(),
  775. label=comment,
  776. )
  777. self.canvas.diagram.AddShape(commentObj)
  778. commentObj.Show(True)
  779. self._addEvent(commentObj)
  780. self.model.AddItem(commentObj)
  781. self.canvas.Refresh()
  782. self.ModelChanged()
  783. dlg.Destroy()
  784. def _switchPageHandler(self, event, notification):
  785. self._switchPage(notification=notification)
  786. event.Skip()
  787. def _switchPage(self, notification):
  788. """Manages @c 'output' notebook page according to event notification."""
  789. if notification == Notification.HIGHLIGHT:
  790. self.notebook.HighlightPageByName("output")
  791. if notification == Notification.MAKE_VISIBLE:
  792. self.notebook.SetSelectionByName("output")
  793. if notification == Notification.RAISE_WINDOW:
  794. self.notebook.SetSelectionByName("output")
  795. self.SetFocus()
  796. self.Raise()
  797. def OnAbout(self, event):
  798. """Display About window"""
  799. ShowAboutDialog(prgName=_("wxGUI Graphical Modeler"), startYear="2010")
  800. def GetOptData(self, dcmd, layer, params, propwin):
  801. """Process action data"""
  802. if params: # add data items
  803. width, height = self.canvas.GetSize()
  804. x = width / 2 - 200 + self._randomShift()
  805. y = height / 2 + self._randomShift()
  806. for p in params["params"]:
  807. if p.get("prompt", "") not in (
  808. "raster",
  809. "vector",
  810. "raster_3d",
  811. "dbtable",
  812. ):
  813. continue
  814. # add new data item if defined or required
  815. if p.get("value", None) or (
  816. p.get("age", "old") != "old" and p.get("required", "no") == "yes"
  817. ):
  818. data = layer.FindData(p.get("name", ""))
  819. if data:
  820. data.SetValue(p.get("value", ""))
  821. data.Update()
  822. continue
  823. data = self.model.FindData(p.get("value", ""), p.get("prompt", ""))
  824. if data:
  825. if p.get("age", "old") == "old":
  826. rel = ModelRelation(
  827. parent=self,
  828. fromShape=data,
  829. toShape=layer,
  830. param=p.get("name", ""),
  831. )
  832. else:
  833. rel = ModelRelation(
  834. parent=self,
  835. fromShape=layer,
  836. toShape=data,
  837. param=p.get("name", ""),
  838. )
  839. layer.AddRelation(rel)
  840. data.AddRelation(rel)
  841. self.AddLine(rel)
  842. data.Update()
  843. continue
  844. data = ModelData(
  845. self,
  846. value=p.get("value", ""),
  847. prompt=p.get("prompt", ""),
  848. x=x,
  849. y=y,
  850. )
  851. self._addEvent(data)
  852. self.canvas.diagram.AddShape(data)
  853. data.Show(True)
  854. if p.get("age", "old") == "old":
  855. rel = ModelRelation(
  856. parent=self,
  857. fromShape=data,
  858. toShape=layer,
  859. param=p.get("name", ""),
  860. )
  861. else:
  862. rel = ModelRelation(
  863. parent=self,
  864. fromShape=layer,
  865. toShape=data,
  866. param=p.get("name", ""),
  867. )
  868. layer.AddRelation(rel)
  869. data.AddRelation(rel)
  870. self.AddLine(rel)
  871. data.Update()
  872. # remove dead data items
  873. if not p.get("value", ""):
  874. data = layer.FindData(p.get("name", ""))
  875. if data:
  876. remList, upList = self.model.RemoveItem(data, layer)
  877. for item in remList:
  878. self.canvas.diagram.RemoveShape(item)
  879. item.__del__()
  880. for item in upList:
  881. item.Update()
  882. # valid / parameterized ?
  883. layer.SetValid(params)
  884. self.canvas.Refresh()
  885. if dcmd:
  886. layer.SetProperties(params, propwin)
  887. self.SetStatusText(layer.GetLog(), 0)
  888. def AddLine(self, rel):
  889. """Add connection between model objects
  890. :param rel: relation
  891. """
  892. fromShape = rel.GetFrom()
  893. toShape = rel.GetTo()
  894. rel.SetCanvas(self)
  895. rel.SetPen(wx.BLACK_PEN)
  896. rel.SetBrush(wx.BLACK_BRUSH)
  897. rel.AddArrow(ogl.ARROW_ARROW)
  898. points = rel.GetControlPoints()
  899. rel.MakeLineControlPoints(2)
  900. if points:
  901. for x, y in points:
  902. rel.InsertLineControlPoint(point=wx.RealPoint(x, y))
  903. self._addEvent(rel)
  904. try:
  905. fromShape.AddLine(rel, toShape)
  906. except TypeError:
  907. pass # bug when connecting ModelCondition and ModelLoop - to be fixed
  908. self.canvas.diagram.AddShape(rel)
  909. rel.Show(True)
  910. def LoadModelFile(self, filename):
  911. """Load model definition stored in GRASS Model XML file (gxm)"""
  912. try:
  913. self.model.LoadModel(filename)
  914. except GException as e:
  915. GError(
  916. parent=self,
  917. message=_(
  918. "Reading model file <%s> failed.\n"
  919. "Invalid file, unable to parse XML document.\n\n%s"
  920. )
  921. % (filename, e),
  922. showTraceback=False,
  923. )
  924. return
  925. self.modelFile = filename
  926. self.SetTitle(self.baseTitle + " - " + os.path.basename(self.modelFile))
  927. self.SetStatusText(_("Please wait, loading model..."), 0)
  928. # load actions
  929. for item in self.model.GetItems(objType=ModelAction):
  930. self._addEvent(item)
  931. self.canvas.diagram.AddShape(item)
  932. item.Show(True)
  933. # relations/data
  934. for rel in item.GetRelations():
  935. if rel.GetFrom() == item:
  936. dataItem = rel.GetTo()
  937. else:
  938. dataItem = rel.GetFrom()
  939. self._addEvent(dataItem)
  940. self.canvas.diagram.AddShape(dataItem)
  941. self.AddLine(rel)
  942. dataItem.Show(True)
  943. # load loops
  944. for item in self.model.GetItems(objType=ModelLoop):
  945. self._addEvent(item)
  946. self.canvas.diagram.AddShape(item)
  947. item.Show(True)
  948. # connect items in the loop
  949. self.DefineLoop(item)
  950. # load conditions
  951. for item in self.model.GetItems(objType=ModelCondition):
  952. self._addEvent(item)
  953. self.canvas.diagram.AddShape(item)
  954. item.Show(True)
  955. # connect items in the condition
  956. self.DefineCondition(item)
  957. # load comments
  958. for item in self.model.GetItems(objType=ModelComment):
  959. self._addEvent(item)
  960. self.canvas.diagram.AddShape(item)
  961. item.Show(True)
  962. # load variables
  963. self.variablePanel.Update()
  964. self.itemPanel.Update()
  965. self.SetStatusText("", 0)
  966. # final updates
  967. for action in self.model.GetItems(objType=ModelAction):
  968. action.SetValid(action.GetParams())
  969. action.Update()
  970. self.canvas.Refresh(True)
  971. def WriteModelFile(self, filename):
  972. """Save model to model file, recover original file on error.
  973. :return: True on success
  974. :return: False on failure
  975. """
  976. self.ModelChanged(False)
  977. tmpfile = tempfile.TemporaryFile(mode="w+")
  978. try:
  979. WriteModelFile(fd=tmpfile, model=self.model)
  980. except Exception:
  981. GError(
  982. parent=self, message=_("Writing current settings to model file failed.")
  983. )
  984. return False
  985. try:
  986. mfile = open(filename, "w")
  987. tmpfile.seek(0)
  988. for line in tmpfile.readlines():
  989. mfile.write(line)
  990. except IOError:
  991. wx.MessageBox(
  992. parent=self,
  993. message=_("Unable to open file <%s> for writing.") % filename,
  994. caption=_("Error"),
  995. style=wx.OK | wx.ICON_ERROR | wx.CENTRE,
  996. )
  997. return False
  998. mfile.close()
  999. return True
  1000. def DefineLoop(self, loop):
  1001. """Define loop with given list of items"""
  1002. parent = loop
  1003. items = loop.GetItems(self.GetModel().GetItems())
  1004. if not items:
  1005. return
  1006. # remove defined relations first
  1007. for rel in loop.GetRelations():
  1008. self.canvas.GetDiagram().RemoveShape(rel)
  1009. loop.Clear()
  1010. for item in items:
  1011. rel = ModelRelation(parent=self, fromShape=parent, toShape=item)
  1012. dx = item.GetX() - parent.GetX()
  1013. dy = item.GetY() - parent.GetY()
  1014. loop.AddRelation(rel)
  1015. if dx != 0:
  1016. rel.SetControlPoints(
  1017. (
  1018. (parent.GetX(), parent.GetY() + dy / 2),
  1019. (parent.GetX() + dx, parent.GetY() + dy / 2),
  1020. )
  1021. )
  1022. self.AddLine(rel)
  1023. parent = item
  1024. # close loop
  1025. item = items[-1]
  1026. rel = ModelRelation(parent=self, fromShape=item, toShape=loop)
  1027. loop.AddRelation(rel)
  1028. self.AddLine(rel)
  1029. dx = (item.GetX() - loop.GetX()) + loop.GetWidth() / 2 + 50
  1030. dy = item.GetHeight() / 2 + 50
  1031. rel.MakeLineControlPoints(0)
  1032. rel.InsertLineControlPoint(
  1033. point=wx.RealPoint(loop.GetX() - loop.GetWidth() / 2, loop.GetY())
  1034. )
  1035. rel.InsertLineControlPoint(
  1036. point=wx.RealPoint(item.GetX(), item.GetY() + item.GetHeight() / 2)
  1037. )
  1038. rel.InsertLineControlPoint(point=wx.RealPoint(item.GetX(), item.GetY() + dy))
  1039. rel.InsertLineControlPoint(
  1040. point=wx.RealPoint(item.GetX() - dx, item.GetY() + dy)
  1041. )
  1042. rel.InsertLineControlPoint(point=wx.RealPoint(item.GetX() - dx, loop.GetY()))
  1043. self.canvas.Refresh()
  1044. def DefineCondition(self, condition):
  1045. """Define if-else statement with given list of items"""
  1046. items = condition.GetItems(self.model.GetItems(objType=ModelAction))
  1047. if not items["if"] and not items["else"]:
  1048. return
  1049. parent = condition
  1050. # remove defined relations first
  1051. for rel in condition.GetRelations():
  1052. self.canvas.GetDiagram().RemoveShape(rel)
  1053. condition.Clear()
  1054. dxIf = condition.GetX() + condition.GetWidth() / 2
  1055. dxElse = condition.GetX() - condition.GetWidth() / 2
  1056. dy = condition.GetY()
  1057. for branch in items.keys():
  1058. for item in items[branch]:
  1059. rel = ModelRelation(parent=self, fromShape=parent, toShape=item)
  1060. condition.AddRelation(rel)
  1061. self.AddLine(rel)
  1062. rel.MakeLineControlPoints(0)
  1063. if branch == "if":
  1064. rel.InsertLineControlPoint(
  1065. point=wx.RealPoint(
  1066. item.GetX() - item.GetWidth() / 2, item.GetY()
  1067. )
  1068. )
  1069. rel.InsertLineControlPoint(point=wx.RealPoint(dxIf, dy))
  1070. else:
  1071. rel.InsertLineControlPoint(point=wx.RealPoint(dxElse, dy))
  1072. rel.InsertLineControlPoint(
  1073. point=wx.RealPoint(
  1074. item.GetX() - item.GetWidth() / 2, item.GetY()
  1075. )
  1076. )
  1077. parent = item
  1078. self.canvas.Refresh()
  1079. class ModelCanvas(ogl.ShapeCanvas):
  1080. """Canvas where model is drawn"""
  1081. def __init__(self, parent):
  1082. self.parent = parent
  1083. ogl.OGLInitialize()
  1084. ogl.ShapeCanvas.__init__(self, parent)
  1085. self.diagram = ogl.Diagram()
  1086. self.SetDiagram(self.diagram)
  1087. self.diagram.SetCanvas(self)
  1088. self.SetScrollbars(20, 20, 2000 // 20, 2000 // 20)
  1089. self.Bind(wx.EVT_KEY_UP, self.OnKeyUp)
  1090. self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
  1091. def OnKeyUp(self, event):
  1092. """Key pressed"""
  1093. kc = event.GetKeyCode()
  1094. if kc == wx.WXK_DELETE:
  1095. self.RemoveSelected()
  1096. def OnLeftDown(self, evt):
  1097. self.SetFocus()
  1098. evt.Skip()
  1099. def RemoveSelected(self):
  1100. """Remove selected shapes"""
  1101. self.parent.ModelChanged()
  1102. diagram = self.GetDiagram()
  1103. shapes = [shape for shape in diagram.GetShapeList() if shape.Selected()]
  1104. self.RemoveShapes(shapes)
  1105. def RemoveShapes(self, shapes):
  1106. """Removes shapes"""
  1107. self.parent.ModelChanged()
  1108. diagram = self.GetDiagram()
  1109. for shape in shapes:
  1110. remList, upList = self.parent.GetModel().RemoveItem(shape)
  1111. shape.Select(False)
  1112. diagram.RemoveShape(shape)
  1113. shape.__del__()
  1114. for item in remList:
  1115. diagram.RemoveShape(item)
  1116. item.__del__()
  1117. for item in upList:
  1118. item.Update()
  1119. self.Refresh()
  1120. def GetNewShapePos(self):
  1121. """Determine optimal position for newly added object
  1122. :return: x,y
  1123. """
  1124. xNew, yNew = map(lambda x: x / 2, self.GetSize())
  1125. diagram = self.GetDiagram()
  1126. for shape in diagram.GetShapeList():
  1127. y = shape.GetY()
  1128. yBox = shape.GetBoundingBoxMin()[1] / 2
  1129. if yBox > 0 and y < yNew + yBox and y > yNew - yBox:
  1130. yNew += yBox * 3
  1131. return xNew, yNew
  1132. def GetShapesSelected(self):
  1133. """Get list of selected shapes"""
  1134. selected = list()
  1135. diagram = self.GetDiagram()
  1136. for shape in diagram.GetShapeList():
  1137. if shape.Selected():
  1138. selected.append(shape)
  1139. return selected
  1140. class ModelEvtHandler(ogl.ShapeEvtHandler):
  1141. """Model event handler class"""
  1142. def __init__(self, log, frame):
  1143. ogl.ShapeEvtHandler.__init__(self)
  1144. self.log = log
  1145. self.frame = frame
  1146. self.x = self.y = None
  1147. def OnLeftClick(self, x, y, keys=0, attachment=0):
  1148. """Left mouse button pressed -> select item & update statusbar"""
  1149. shape = self.GetShape()
  1150. canvas = shape.GetCanvas()
  1151. dc = wx.ClientDC(canvas)
  1152. # probably does nothing, removed from wxPython 2.9
  1153. # canvas.PrepareDC(dc)
  1154. if hasattr(self.frame, "defineRelation"):
  1155. drel = self.frame.defineRelation
  1156. if drel["from"] is None:
  1157. drel["from"] = shape
  1158. elif drel["to"] is None:
  1159. drel["to"] = shape
  1160. rel = ModelRelation(
  1161. parent=self.frame, fromShape=drel["from"], toShape=drel["to"]
  1162. )
  1163. dlg = ModelRelationDialog(parent=self.frame, shape=rel)
  1164. if dlg.IsValid():
  1165. ret = dlg.ShowModal()
  1166. if ret == wx.ID_OK:
  1167. option = dlg.GetOption()
  1168. rel.SetName(option)
  1169. drel["from"].AddRelation(rel)
  1170. drel["to"].AddRelation(rel)
  1171. drel["from"].Update()
  1172. params = {
  1173. "params": [
  1174. {"name": option, "value": drel["from"].GetValue()}
  1175. ]
  1176. }
  1177. drel["to"].MergeParams(params)
  1178. self.frame.AddLine(rel)
  1179. dlg.Destroy()
  1180. del self.frame.defineRelation
  1181. # select object
  1182. self._onSelectShape(shape, append=True if keys == 1 else False)
  1183. if hasattr(shape, "GetLog"):
  1184. self.log.SetStatusText(shape.GetLog(), 0)
  1185. else:
  1186. self.log.SetStatusText("", 0)
  1187. def OnLeftDoubleClick(self, x, y, keys=0, attachment=0):
  1188. """Left mouse button pressed (double-click) -> show properties"""
  1189. self.OnProperties()
  1190. def OnProperties(self, event=None):
  1191. """Show properties dialog"""
  1192. self.frame.ModelChanged()
  1193. shape = self.GetShape()
  1194. if isinstance(shape, ModelAction):
  1195. gmodule = GUI(
  1196. parent=self.frame,
  1197. show=True,
  1198. giface=GraphicalModelerGrassInterface(self.frame.GetModel()),
  1199. )
  1200. gmodule.ParseCommand(
  1201. shape.GetLog(string=False),
  1202. completed=(self.frame.GetOptData, shape, shape.GetParams()),
  1203. )
  1204. elif isinstance(shape, ModelData):
  1205. if shape.GetPrompt() in ("raster", "vector", "raster_3d"):
  1206. dlg = ModelDataDialog(parent=self.frame, shape=shape)
  1207. shape.SetPropDialog(dlg)
  1208. dlg.CentreOnParent()
  1209. dlg.Show()
  1210. elif isinstance(shape, ModelLoop):
  1211. dlg = ModelLoopDialog(parent=self.frame, shape=shape)
  1212. dlg.CentreOnParent()
  1213. if dlg.ShowModal() == wx.ID_OK:
  1214. shape.SetLabel(dlg.GetCondition())
  1215. model = self.frame.GetModel()
  1216. ids = dlg.GetItems()
  1217. alist = list()
  1218. for aId in ids["unchecked"]:
  1219. action = model.GetItem(aId, objType=ModelAction)
  1220. if action:
  1221. action.UnSetBlock(shape)
  1222. for aId in ids["checked"]:
  1223. action = model.GetItem(aId, objType=ModelAction)
  1224. if action:
  1225. action.SetBlock(shape)
  1226. alist.append(aId)
  1227. shape.SetItems(alist)
  1228. self.frame.DefineLoop(shape)
  1229. self.frame.SetStatusText(shape.GetLog(), 0)
  1230. self.frame.GetCanvas().Refresh()
  1231. dlg.Destroy()
  1232. elif isinstance(shape, ModelCondition):
  1233. dlg = ModelConditionDialog(parent=self.frame, shape=shape)
  1234. dlg.CentreOnParent()
  1235. if dlg.ShowModal() == wx.ID_OK:
  1236. shape.SetLabel(dlg.GetCondition())
  1237. model = self.frame.GetModel()
  1238. ids = dlg.GetItems()
  1239. for b in ids.keys():
  1240. alist = list()
  1241. for aId in ids[b]["unchecked"]:
  1242. action = model.GetItem(aId, objType=ModelAction)
  1243. action.UnSetBlock(shape)
  1244. for aId in ids[b]["checked"]:
  1245. action = model.GetItem(aId, objType=ModelAction)
  1246. action.SetBlock(shape)
  1247. if action:
  1248. alist.append(aId)
  1249. shape.SetItems(alist, branch=b)
  1250. self.frame.DefineCondition(shape)
  1251. self.frame.GetCanvas().Refresh()
  1252. dlg.Destroy()
  1253. def OnBeginDragLeft(self, x, y, keys=0, attachment=0):
  1254. """Drag shape (beginning)"""
  1255. self.frame.ModelChanged()
  1256. if self._previousHandler:
  1257. self._previousHandler.OnBeginDragLeft(x, y, keys, attachment)
  1258. def OnEndDragLeft(self, x, y, keys=0, attachment=0):
  1259. """Drag shape (end)"""
  1260. if self._previousHandler:
  1261. self._previousHandler.OnEndDragLeft(x, y, keys, attachment)
  1262. shape = self.GetShape()
  1263. if isinstance(shape, ModelLoop):
  1264. self.frame.DefineLoop(shape)
  1265. elif isinstance(shape, ModelCondition):
  1266. self.frame.DefineCondition(shape)
  1267. for mo in shape.GetBlock():
  1268. if isinstance(mo, ModelLoop):
  1269. self.frame.DefineLoop(mo)
  1270. elif isinstance(mo, ModelCondition):
  1271. self.frame.DefineCondition(mo)
  1272. shape = self.GetShape()
  1273. canvas = shape.GetCanvas()
  1274. canvas.Refresh()
  1275. def OnEndSize(self, x, y):
  1276. """Resize shape"""
  1277. self.frame.ModelChanged()
  1278. if self._previousHandler:
  1279. self._previousHandler.OnEndSize(x, y)
  1280. def OnRightClick(self, x, y, keys=0, attachment=0):
  1281. """Right click -> pop-up menu"""
  1282. if not hasattr(self, "popupID"):
  1283. self.popupID = dict()
  1284. for key in (
  1285. "remove",
  1286. "enable",
  1287. "addPoint",
  1288. "delPoint",
  1289. "intermediate",
  1290. "display",
  1291. "props",
  1292. "id",
  1293. "label",
  1294. "comment",
  1295. ):
  1296. self.popupID[key] = NewId()
  1297. # record coordinates
  1298. self.x = x
  1299. self.y = y
  1300. # select object
  1301. shape = self.GetShape()
  1302. self._onSelectShape(shape)
  1303. popupMenu = Menu()
  1304. popupMenu.Append(self.popupID["remove"], _("Remove"))
  1305. self.frame.Bind(wx.EVT_MENU, self.OnRemove, id=self.popupID["remove"])
  1306. if isinstance(shape, ModelAction) or isinstance(shape, ModelLoop):
  1307. if shape.IsEnabled():
  1308. popupMenu.Append(self.popupID["enable"], _("Disable"))
  1309. self.frame.Bind(wx.EVT_MENU, self.OnDisable, id=self.popupID["enable"])
  1310. else:
  1311. popupMenu.Append(self.popupID["enable"], _("Enable"))
  1312. self.frame.Bind(wx.EVT_MENU, self.OnEnable, id=self.popupID["enable"])
  1313. if isinstance(shape, ModelAction) or isinstance(shape, ModelComment):
  1314. popupMenu.AppendSeparator()
  1315. if isinstance(shape, ModelAction):
  1316. popupMenu.Append(self.popupID["label"], _("Set label"))
  1317. self.frame.Bind(wx.EVT_MENU, self.OnSetLabel, id=self.popupID["label"])
  1318. if isinstance(shape, ModelAction) or isinstance(shape, ModelComment):
  1319. popupMenu.Append(self.popupID["comment"], _("Set comment"))
  1320. self.frame.Bind(wx.EVT_MENU, self.OnSetComment, id=self.popupID["comment"])
  1321. if isinstance(shape, ModelRelation):
  1322. popupMenu.AppendSeparator()
  1323. popupMenu.Append(self.popupID["addPoint"], _("Add control point"))
  1324. self.frame.Bind(wx.EVT_MENU, self.OnAddPoint, id=self.popupID["addPoint"])
  1325. popupMenu.Append(self.popupID["delPoint"], _("Remove control point"))
  1326. self.frame.Bind(
  1327. wx.EVT_MENU, self.OnRemovePoint, id=self.popupID["delPoint"]
  1328. )
  1329. if len(shape.GetLineControlPoints()) == 2:
  1330. popupMenu.Enable(self.popupID["delPoint"], False)
  1331. if isinstance(shape, ModelData):
  1332. popupMenu.AppendSeparator()
  1333. if (
  1334. "@" not in shape.GetValue()
  1335. and len(self.GetShape().GetRelations("from")) > 0
  1336. ):
  1337. popupMenu.Append(
  1338. self.popupID["intermediate"], _("Intermediate"), kind=wx.ITEM_CHECK
  1339. )
  1340. if self.GetShape().IsIntermediate():
  1341. popupMenu.Check(self.popupID["intermediate"], True)
  1342. self.frame.Bind(
  1343. wx.EVT_MENU, self.OnIntermediate, id=self.popupID["intermediate"]
  1344. )
  1345. if self.frame._giface.GetMapDisplay():
  1346. popupMenu.Append(
  1347. self.popupID["display"], _("Display"), kind=wx.ITEM_CHECK
  1348. )
  1349. if self.GetShape().HasDisplay():
  1350. popupMenu.Check(self.popupID["display"], True)
  1351. self.frame.Bind(
  1352. wx.EVT_MENU, self.OnHasDisplay, id=self.popupID["display"]
  1353. )
  1354. if self.GetShape().IsIntermediate():
  1355. popupMenu.Enable(self.popupID["display"], False)
  1356. if (
  1357. isinstance(shape, ModelData)
  1358. or isinstance(shape, ModelAction)
  1359. or isinstance(shape, ModelLoop)
  1360. ):
  1361. popupMenu.AppendSeparator()
  1362. popupMenu.Append(self.popupID["props"], _("Properties"))
  1363. self.frame.Bind(wx.EVT_MENU, self.OnProperties, id=self.popupID["props"])
  1364. self.frame.PopupMenu(popupMenu)
  1365. popupMenu.Destroy()
  1366. def OnDisable(self, event):
  1367. """Disable action"""
  1368. self._onEnable(False)
  1369. def OnEnable(self, event):
  1370. """Disable action"""
  1371. self._onEnable(True)
  1372. def _onEnable(self, enable):
  1373. shape = self.GetShape()
  1374. shape.Enable(enable)
  1375. self.frame.ModelChanged()
  1376. self.frame.canvas.Refresh()
  1377. def OnSetLabel(self, event):
  1378. shape = self.GetShape()
  1379. dlg = wxTextEntryDialog(
  1380. parent=self.frame,
  1381. message=_("Label:"),
  1382. caption=_("Set label"),
  1383. value=shape.GetLabel(),
  1384. )
  1385. if dlg.ShowModal() == wx.ID_OK:
  1386. label = dlg.GetValue()
  1387. shape.SetLabel(label)
  1388. self.frame.ModelChanged()
  1389. self.frame.itemPanel.Update()
  1390. self.frame.canvas.Refresh()
  1391. dlg.Destroy()
  1392. def OnSetComment(self, event):
  1393. shape = self.GetShape()
  1394. dlg = CustomTextEntryDialog(
  1395. parent=self.frame,
  1396. message=_("Comment:"),
  1397. caption=_("Set comment"),
  1398. defaultValue=shape.GetComment(),
  1399. textStyle=wx.TE_MULTILINE,
  1400. textSize=(300, 75),
  1401. )
  1402. if dlg.ShowModal() == wx.ID_OK:
  1403. comment = dlg.GetValue()
  1404. shape.SetComment(comment)
  1405. self.frame.ModelChanged()
  1406. self.frame.canvas.Refresh()
  1407. dlg.Destroy()
  1408. def _onSelectShape(self, shape, append=False):
  1409. canvas = shape.GetCanvas()
  1410. dc = wx.ClientDC(canvas)
  1411. if shape.Selected():
  1412. shape.Select(False, dc)
  1413. else:
  1414. redraw = False
  1415. shapeList = canvas.GetDiagram().GetShapeList()
  1416. toUnselect = list()
  1417. if not append:
  1418. for s in shapeList:
  1419. if s.Selected():
  1420. toUnselect.append(s)
  1421. shape.Select(True, dc)
  1422. for s in toUnselect:
  1423. s.Select(False, dc)
  1424. canvas.Refresh(False)
  1425. def OnAddPoint(self, event):
  1426. """Add control point"""
  1427. shape = self.GetShape()
  1428. shape.InsertLineControlPoint(point=wx.RealPoint(self.x, self.y))
  1429. shape.ResetShapes()
  1430. shape.Select(True)
  1431. self.frame.ModelChanged()
  1432. self.frame.canvas.Refresh()
  1433. def OnRemovePoint(self, event):
  1434. """Remove control point"""
  1435. shape = self.GetShape()
  1436. shape.DeleteLineControlPoint()
  1437. shape.Select(False)
  1438. shape.Select(True)
  1439. self.frame.ModelChanged()
  1440. self.frame.canvas.Refresh()
  1441. def OnIntermediate(self, event):
  1442. """Mark data as intermediate"""
  1443. self.frame.ModelChanged()
  1444. shape = self.GetShape()
  1445. shape.SetIntermediate(event.IsChecked())
  1446. self.frame.canvas.Refresh()
  1447. def OnHasDisplay(self, event):
  1448. """Mark data to be displayed"""
  1449. self.frame.ModelChanged()
  1450. shape = self.GetShape()
  1451. shape.SetHasDisplay(event.IsChecked())
  1452. self.frame.canvas.Refresh()
  1453. try:
  1454. if event.IsChecked():
  1455. # add map layer to display
  1456. self.frame._giface.GetLayerList().AddLayer(
  1457. ltype=shape.GetPrompt(),
  1458. name=shape.GetValue(),
  1459. checked=True,
  1460. cmd=shape.GetDisplayCmd(),
  1461. )
  1462. else:
  1463. # remove map layer(s) from display
  1464. layers = self.frame._giface.GetLayerList().GetLayersByName(
  1465. shape.GetValue()
  1466. )
  1467. for layer in layers:
  1468. self.frame._giface.GetLayerList().DeleteLayer(layer)
  1469. except GException as e:
  1470. GError(parent=self, message="{}".format(e))
  1471. def OnRemove(self, event):
  1472. """Remove shape"""
  1473. self.frame.GetCanvas().RemoveShapes([self.GetShape()])
  1474. self.frame.itemPanel.Update()
  1475. class VariablePanel(wx.Panel):
  1476. def __init__(self, parent, id=wx.ID_ANY, **kwargs):
  1477. """Manage model variables panel"""
  1478. self.parent = parent
  1479. wx.Panel.__init__(self, parent=parent, id=id, **kwargs)
  1480. self.listBox = StaticBox(
  1481. parent=self,
  1482. id=wx.ID_ANY,
  1483. label=" %s " % _("List of variables - right-click to delete"),
  1484. )
  1485. self.list = VariableListCtrl(
  1486. parent=self,
  1487. columns=[_("Name"), _("Data type"), _("Default value"), _("Description")],
  1488. frame=self.parent,
  1489. )
  1490. # add new category
  1491. self.addBox = StaticBox(
  1492. parent=self, id=wx.ID_ANY, label=" %s " % _("Add new variable")
  1493. )
  1494. self.name = TextCtrl(parent=self, id=wx.ID_ANY)
  1495. wx.CallAfter(self.name.SetFocus)
  1496. self.type = wx.Choice(
  1497. parent=self,
  1498. id=wx.ID_ANY,
  1499. choices=[
  1500. _("integer"),
  1501. _("float"),
  1502. _("string"),
  1503. _("raster"),
  1504. _("vector"),
  1505. _("region"),
  1506. _("mapset"),
  1507. _("file"),
  1508. _("dir"),
  1509. ],
  1510. )
  1511. self.type.SetSelection(2) # string
  1512. self.value = TextCtrl(parent=self, id=wx.ID_ANY)
  1513. self.desc = TextCtrl(parent=self, id=wx.ID_ANY)
  1514. # buttons
  1515. self.btnAdd = Button(parent=self, id=wx.ID_ADD)
  1516. self.btnAdd.SetToolTip(_("Add new variable to the model"))
  1517. self.btnAdd.Enable(False)
  1518. # bindings
  1519. self.name.Bind(wx.EVT_TEXT, self.OnText)
  1520. self.value.Bind(wx.EVT_TEXT, self.OnText)
  1521. self.desc.Bind(wx.EVT_TEXT, self.OnText)
  1522. self.btnAdd.Bind(wx.EVT_BUTTON, self.OnAdd)
  1523. self._layout()
  1524. def _layout(self):
  1525. """Layout dialog"""
  1526. listSizer = wx.StaticBoxSizer(self.listBox, wx.VERTICAL)
  1527. listSizer.Add(self.list, proportion=1, flag=wx.EXPAND)
  1528. addSizer = wx.StaticBoxSizer(self.addBox, wx.VERTICAL)
  1529. gridSizer = wx.GridBagSizer(hgap=5, vgap=5)
  1530. gridSizer.Add(
  1531. StaticText(parent=self, id=wx.ID_ANY, label="%s:" % _("Name")),
  1532. flag=wx.ALIGN_CENTER_VERTICAL,
  1533. pos=(0, 0),
  1534. )
  1535. gridSizer.Add(self.name, pos=(0, 1), flag=wx.EXPAND)
  1536. gridSizer.Add(
  1537. StaticText(parent=self, id=wx.ID_ANY, label="%s:" % _("Data type")),
  1538. flag=wx.ALIGN_CENTER_VERTICAL,
  1539. pos=(0, 2),
  1540. )
  1541. gridSizer.Add(self.type, pos=(0, 3))
  1542. gridSizer.Add(
  1543. StaticText(parent=self, id=wx.ID_ANY, label="%s:" % _("Default value")),
  1544. flag=wx.ALIGN_CENTER_VERTICAL,
  1545. pos=(1, 0),
  1546. )
  1547. gridSizer.Add(self.value, pos=(1, 1), span=(1, 3), flag=wx.EXPAND)
  1548. gridSizer.Add(
  1549. StaticText(parent=self, id=wx.ID_ANY, label="%s:" % _("Description")),
  1550. flag=wx.ALIGN_CENTER_VERTICAL,
  1551. pos=(2, 0),
  1552. )
  1553. gridSizer.Add(self.desc, pos=(2, 1), span=(1, 3), flag=wx.EXPAND)
  1554. gridSizer.AddGrowableCol(1)
  1555. addSizer.Add(gridSizer, flag=wx.EXPAND)
  1556. addSizer.Add(self.btnAdd, proportion=0, flag=wx.TOP | wx.ALIGN_RIGHT, border=5)
  1557. mainSizer = wx.BoxSizer(wx.VERTICAL)
  1558. mainSizer.Add(listSizer, proportion=1, flag=wx.EXPAND | wx.ALL, border=5)
  1559. mainSizer.Add(
  1560. addSizer,
  1561. proportion=0,
  1562. flag=wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM,
  1563. border=5,
  1564. )
  1565. self.SetSizer(mainSizer)
  1566. mainSizer.Fit(self)
  1567. def OnText(self, event):
  1568. """Text entered"""
  1569. if self.name.GetValue():
  1570. self.btnAdd.Enable()
  1571. else:
  1572. self.btnAdd.Enable(False)
  1573. def OnAdd(self, event):
  1574. """Add new variable to the list"""
  1575. msg = self.list.Append(
  1576. self.name.GetValue(),
  1577. self.type.GetStringSelection(),
  1578. self.value.GetValue(),
  1579. self.desc.GetValue(),
  1580. )
  1581. self.name.SetValue("")
  1582. self.name.SetFocus()
  1583. if msg:
  1584. GError(parent=self, message=msg)
  1585. else:
  1586. self.type.SetSelection(2) # string
  1587. self.value.SetValue("")
  1588. self.desc.SetValue("")
  1589. self.UpdateModelVariables()
  1590. def UpdateModelVariables(self):
  1591. """Update model variables"""
  1592. variables = dict()
  1593. for values in six.itervalues(self.list.GetData()):
  1594. name = values[0]
  1595. variables[name] = {"type": str(values[1])}
  1596. if values[2]:
  1597. variables[name]["value"] = values[2]
  1598. if values[3]:
  1599. variables[name]["description"] = values[3]
  1600. self.parent.GetModel().SetVariables(variables)
  1601. self.parent.ModelChanged()
  1602. def Update(self):
  1603. """Reload list of variables"""
  1604. self.list.OnReload(None)
  1605. def Reset(self):
  1606. """Remove all variables"""
  1607. self.list.DeleteAllItems()
  1608. self.parent.GetModel().SetVariables([])
  1609. class ItemPanel(wx.Panel):
  1610. def __init__(self, parent, id=wx.ID_ANY, **kwargs):
  1611. """Manage model items"""
  1612. self.parent = parent
  1613. wx.Panel.__init__(self, parent=parent, id=id, **kwargs)
  1614. self.listBox = StaticBox(
  1615. parent=self,
  1616. id=wx.ID_ANY,
  1617. label=" %s " % _("List of items - right-click to delete"),
  1618. )
  1619. self.list = ItemListCtrl(
  1620. parent=self,
  1621. columns=[_("Label"), _("In loop"), _("Parameterized"), _("Command")],
  1622. columnsNotEditable=[1, 2, 3],
  1623. frame=self.parent,
  1624. )
  1625. self.btnMoveUp = Button(parent=self, id=wx.ID_UP)
  1626. self.btnMoveDown = Button(parent=self, id=wx.ID_DOWN)
  1627. self.btnRefresh = Button(parent=self, id=wx.ID_REFRESH)
  1628. self.btnMoveUp.Bind(wx.EVT_BUTTON, self.OnMoveItemsUp)
  1629. self.btnMoveDown.Bind(wx.EVT_BUTTON, self.OnMoveItemsDown)
  1630. self.btnRefresh.Bind(wx.EVT_BUTTON, self.list.OnReload)
  1631. self._layout()
  1632. def _layout(self):
  1633. """Layout dialog"""
  1634. listSizer = wx.StaticBoxSizer(self.listBox, wx.VERTICAL)
  1635. listSizer.Add(self.list, proportion=1, flag=wx.EXPAND)
  1636. manageSizer = wx.BoxSizer(wx.VERTICAL)
  1637. manageSizer.Add(self.btnMoveUp, border=5, flag=wx.ALL)
  1638. manageSizer.Add(self.btnMoveDown, border=5, flag=wx.LEFT | wx.RIGHT | wx.BOTTOM)
  1639. manageSizer.Add(self.btnRefresh, border=5, flag=wx.LEFT | wx.RIGHT)
  1640. mainSizer = wx.BoxSizer(wx.HORIZONTAL)
  1641. mainSizer.Add(listSizer, proportion=1, flag=wx.EXPAND | wx.ALL, border=3)
  1642. mainSizer.Add(manageSizer, proportion=0, flag=wx.EXPAND | wx.ALL, border=3)
  1643. self.SetSizer(mainSizer)
  1644. mainSizer.Fit(self)
  1645. def Update(self):
  1646. """Reload list of variables"""
  1647. self.list.OnReload(None)
  1648. def _getSelectedItems(self):
  1649. """Get list of selected items, indices start at 0"""
  1650. items = []
  1651. current = -1
  1652. while True:
  1653. next = self.list.GetNextSelected(current)
  1654. if next == -1:
  1655. break
  1656. items.append(next)
  1657. current = next
  1658. if not items:
  1659. GMessage(_("No items to selected."), parent=self)
  1660. return items
  1661. def OnMoveItemsUp(self, event):
  1662. """Item moved up, update action ids"""
  1663. items = self._getSelectedItems()
  1664. if not items:
  1665. return
  1666. self.list.MoveItems(items, up=True)
  1667. self.parent.GetCanvas().Refresh()
  1668. self.parent.ModelChanged()
  1669. def OnMoveItemsDown(self, event):
  1670. """Item moved up, update action ids"""
  1671. items = self._getSelectedItems()
  1672. if not items:
  1673. return
  1674. self.list.MoveItems(items, up=False)
  1675. self.parent.GetCanvas().Refresh()
  1676. self.parent.ModelChanged()
  1677. class PythonPanel(wx.Panel):
  1678. def __init__(self, parent, id=wx.ID_ANY, **kwargs):
  1679. """Model as python script"""
  1680. self.parent = parent
  1681. wx.Panel.__init__(self, parent=parent, id=id, **kwargs)
  1682. self.filename = None # temp file to run
  1683. self.bodyBox = StaticBox(
  1684. parent=self, id=wx.ID_ANY, label=" %s " % _("Python script")
  1685. )
  1686. self.body = PyStc(parent=self, statusbar=self.parent.GetStatusBar())
  1687. if IsDark():
  1688. SetDarkMode(self.body)
  1689. self.btnRun = Button(parent=self, id=wx.ID_ANY, label=_("&Run"))
  1690. self.btnRun.SetToolTip(_("Run python script"))
  1691. self.Bind(wx.EVT_BUTTON, self.OnRun, self.btnRun)
  1692. self.btnSaveAs = Button(parent=self, id=wx.ID_SAVEAS)
  1693. self.btnSaveAs.SetToolTip(_("Save python script to file"))
  1694. self.Bind(wx.EVT_BUTTON, self.OnSaveAs, self.btnSaveAs)
  1695. self.btnRefresh = Button(parent=self, id=wx.ID_REFRESH)
  1696. self.btnRefresh.SetToolTip(
  1697. _(
  1698. "Refresh python script based on the model.\n"
  1699. "It will discards all local changes."
  1700. )
  1701. )
  1702. self.Bind(wx.EVT_BUTTON, self.OnRefresh, self.btnRefresh)
  1703. self._layout()
  1704. def _layout(self):
  1705. sizer = wx.BoxSizer(wx.VERTICAL)
  1706. bodySizer = wx.StaticBoxSizer(self.bodyBox, wx.HORIZONTAL)
  1707. btnSizer = wx.BoxSizer(wx.HORIZONTAL)
  1708. bodySizer.Add(self.body, proportion=1, flag=wx.EXPAND | wx.ALL, border=3)
  1709. btnSizer.Add(self.btnRefresh, proportion=0, flag=wx.LEFT | wx.RIGHT, border=5)
  1710. btnSizer.AddStretchSpacer()
  1711. btnSizer.Add(self.btnSaveAs, proportion=0, flag=wx.RIGHT, border=5)
  1712. btnSizer.Add(self.btnRun, proportion=0, flag=wx.RIGHT, border=5)
  1713. sizer.Add(bodySizer, proportion=1, flag=wx.EXPAND | wx.ALL, border=3)
  1714. sizer.Add(btnSizer, proportion=0, flag=wx.EXPAND | wx.ALL, border=3)
  1715. sizer.Fit(self)
  1716. sizer.SetSizeHints(self)
  1717. self.SetSizer(sizer)
  1718. def OnRun(self, event):
  1719. """Run Python script"""
  1720. self.filename = grass.tempfile()
  1721. try:
  1722. fd = open(self.filename, "w")
  1723. fd.write(self.body.GetText())
  1724. except IOError as e:
  1725. GError(_("Unable to launch Python script. %s") % e, parent=self)
  1726. return
  1727. finally:
  1728. fd.close()
  1729. mode = stat.S_IMODE(os.lstat(self.filename)[stat.ST_MODE])
  1730. os.chmod(self.filename, mode | stat.S_IXUSR)
  1731. for item in self.parent.GetModel().GetItems():
  1732. if (
  1733. len(item.GetParameterizedParams()["params"])
  1734. + len(item.GetParameterizedParams()["flags"])
  1735. > 0
  1736. ):
  1737. self.parent._gconsole.RunCmd(
  1738. [fd.name, "--ui"], skipInterface=False, onDone=self.OnDone
  1739. )
  1740. break
  1741. else:
  1742. self.parent._gconsole.RunCmd(
  1743. [fd.name], skipInterface=True, onDone=self.OnDone
  1744. )
  1745. event.Skip()
  1746. def OnDone(self, event):
  1747. """Python script finished"""
  1748. try_remove(self.filename)
  1749. self.filename = None
  1750. def SaveAs(self, force=False):
  1751. """Save python script to file
  1752. :return: filename
  1753. """
  1754. filename = ""
  1755. dlg = wx.FileDialog(
  1756. parent=self,
  1757. message=_("Choose file to save"),
  1758. defaultFile=os.path.basename(self.parent.GetModelFile(ext=False)),
  1759. defaultDir=os.getcwd(),
  1760. wildcard=_("Python script (*.py)|*.py"),
  1761. style=wx.FD_SAVE,
  1762. )
  1763. if dlg.ShowModal() == wx.ID_OK:
  1764. filename = dlg.GetPath()
  1765. if not filename:
  1766. return ""
  1767. # check for extension
  1768. if filename[-3:] != ".py":
  1769. filename += ".py"
  1770. if os.path.exists(filename):
  1771. dlg = wx.MessageDialog(
  1772. self,
  1773. message=_(
  1774. "File <%s> already exists. " "Do you want to overwrite this file?"
  1775. )
  1776. % filename,
  1777. caption=_("Save file"),
  1778. style=wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION,
  1779. )
  1780. if dlg.ShowModal() == wx.ID_NO:
  1781. dlg.Destroy()
  1782. return ""
  1783. dlg.Destroy()
  1784. fd = open(filename, "w")
  1785. try:
  1786. if force:
  1787. WritePythonFile(fd, self.parent.GetModel())
  1788. else:
  1789. fd.write(self.body.GetText())
  1790. finally:
  1791. fd.close()
  1792. # executable file
  1793. os.chmod(filename, stat.S_IRWXU | stat.S_IWUSR)
  1794. return filename
  1795. def OnSaveAs(self, event):
  1796. """Save python script to file"""
  1797. self.SaveAs(force=False)
  1798. event.Skip()
  1799. def RefreshScript(self):
  1800. """Refresh Python script
  1801. :return: True on refresh
  1802. :return: False script hasn't been updated
  1803. """
  1804. if self.body.modified:
  1805. dlg = wx.MessageDialog(
  1806. self,
  1807. message=_(
  1808. "Python script is locally modificated. "
  1809. "Refresh will discard all changes. "
  1810. "Do you really want to continue?"
  1811. ),
  1812. caption=_("Update"),
  1813. style=wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION | wx.CENTRE,
  1814. )
  1815. ret = dlg.ShowModal()
  1816. dlg.Destroy()
  1817. if ret == wx.ID_NO:
  1818. return False
  1819. fd = tempfile.TemporaryFile(mode="r+")
  1820. WritePythonFile(fd, self.parent.GetModel())
  1821. fd.seek(0)
  1822. self.body.SetText(fd.read())
  1823. fd.close()
  1824. self.body.modified = False
  1825. return True
  1826. def OnRefresh(self, event):
  1827. """Refresh Python script"""
  1828. if self.RefreshScript():
  1829. self.parent.SetStatusText(_("Python script is up-to-date"), 0)
  1830. event.Skip()
  1831. def IsModified(self):
  1832. """Check if python script has been modified"""
  1833. return self.body.modified
  1834. def IsEmpty(self):
  1835. """Check if python script is empty"""
  1836. return len(self.body.GetText()) == 0