frame.py 68 KB

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