frame.py 72 KB

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