goutput.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575
  1. """
  2. @package goutput
  3. @brief Command output log widget
  4. Classes:
  5. - GMConsole
  6. - GMStc
  7. - GMStdout
  8. - GMStderr
  9. (C) 2007-2008 by the GRASS Development Team
  10. This program is free software under the GNU General Public
  11. License (>=v2). Read the file COPYING that comes with GRASS
  12. for details.
  13. @author Michael Barton (Arizona State University)
  14. Martin Landa <landa.martin gmail.com>
  15. """
  16. import os
  17. import sys
  18. import textwrap
  19. import time
  20. import wx
  21. import wx.stc
  22. import globalvar
  23. import gcmd
  24. from debug import Debug as Debug
  25. class GMConsole(wx.Panel):
  26. """
  27. Create and manage output console for commands entered on the
  28. GIS Manager command line.
  29. """
  30. def __init__(self, parent, id=wx.ID_ANY, margin=False, pageid=0,
  31. pos=wx.DefaultPosition, size=wx.DefaultSize,
  32. style=wx.TAB_TRAVERSAL | wx.FULL_REPAINT_ON_RESIZE):
  33. wx.Panel.__init__(self, parent, id, pos, size, style)
  34. # initialize variables
  35. self.Map = None
  36. self.parent = parent # GMFrame
  37. self.cmdThreads = {} # cmdThread : cmdPID
  38. self.lineWidth = 80
  39. self.pageid = pageid
  40. # progress bar
  41. self.console_progressbar = wx.Gauge(parent=self, id=wx.ID_ANY,
  42. range=100, pos=(110, 50), size=(-1, 25),
  43. style=wx.GA_HORIZONTAL)
  44. # text control for command output
  45. self.cmd_output = GMStc(parent=self, id=wx.ID_ANY, margin=margin,
  46. wrap=None)
  47. # redirect
  48. self.cmd_stdout = GMStdout(self.cmd_output)
  49. ### sys.stdout = self.cmd_stdout
  50. self.cmd_stderr = GMStderr(self.cmd_output,
  51. self.console_progressbar,
  52. self.parent.notebook,
  53. pageid)
  54. # buttons
  55. self.console_clear = wx.Button(parent=self, id=wx.ID_CLEAR)
  56. self.console_save = wx.Button(parent=self, id=wx.ID_SAVE)
  57. self.Bind(wx.EVT_BUTTON, self.ClearHistory, self.console_clear)
  58. self.Bind(wx.EVT_BUTTON, self.SaveHistory, self.console_save)
  59. # output control layout
  60. boxsizer1 = wx.BoxSizer(wx.VERTICAL)
  61. gridsizer1 = wx.GridSizer(rows=1, cols=2, vgap=0, hgap=0)
  62. boxsizer1.Add(item=self.cmd_output, proportion=1,
  63. flag=wx.EXPAND | wx.ADJUST_MINSIZE, border=0)
  64. gridsizer1.Add(item=self.console_clear, proportion=0,
  65. flag=wx.ALIGN_CENTER_HORIZONTAL | wx.ADJUST_MINSIZE, border=0)
  66. gridsizer1.Add(item=self.console_save, proportion=0,
  67. flag=wx.ALIGN_CENTER_HORIZONTAL | wx.ADJUST_MINSIZE, border=0)
  68. boxsizer1.Add(item=gridsizer1, proportion=0,
  69. flag=wx.EXPAND | wx.ALIGN_CENTRE_VERTICAL | wx.TOP | wx.BOTTOM,
  70. border=5)
  71. boxsizer1.Add(item=self.console_progressbar, proportion=0,
  72. flag=wx.EXPAND | wx.ADJUST_MINSIZE | wx.LEFT | wx.RIGHT | wx.BOTTOM, border=5)
  73. boxsizer1.Fit(self)
  74. boxsizer1.SetSizeHints(self)
  75. # set up event handler for any command thread results
  76. gcmd.EVT_RESULT(self, self.OnResult)
  77. # layout
  78. self.SetAutoLayout(True)
  79. self.SetSizer(boxsizer1)
  80. def Redirect(self):
  81. """Redirect stderr
  82. @return True redirected
  83. @return False failed
  84. """
  85. if Debug.get_level() == 0:
  86. # don't redirect when debugging is enabled
  87. # sys.stderr = self.cmd_stderr
  88. pass
  89. return True
  90. return False
  91. def WriteLog(self, line, style=None, wrap=None):
  92. """Generic method for writing log message in
  93. given style
  94. @param line text line
  95. @param style text style (see GMStc)
  96. @param stdout write to stdout or stderr
  97. """
  98. if not style:
  99. style = self.cmd_output.StyleDefault
  100. self.cmd_output.GotoPos(self.cmd_output.GetEndStyled())
  101. p1 = self.cmd_output.GetCurrentPos()
  102. # fill space
  103. if len(line) < self.lineWidth:
  104. diff = 80 - len(line)
  105. line += diff * ' '
  106. self.cmd_output.AddTextWrapped(line, wrap=wrap) # adds os.linesep
  107. self.cmd_output.EnsureCaretVisible()
  108. p2 = self.cmd_output.GetCurrentPos()
  109. self.cmd_output.StartStyling(p1, 0xff)
  110. self.cmd_output.SetStyling(p2 - p1, style)
  111. def WriteCmdLog(self, line, pid=None):
  112. """Write out line in selected style"""
  113. if pid:
  114. line = '(' + str(pid) + ') ' + line
  115. self.WriteLog(line, self.cmd_output.StyleCommand)
  116. def RunCmd(self, command):
  117. """
  118. Run in GUI GRASS (or other) commands typed into
  119. console command text widget, and send stdout output to output
  120. text widget.
  121. Command is transformed into a list for processing.
  122. TODO: Display commands (*.d) are captured and
  123. processed separately by mapdisp.py. Display commands are
  124. rendered in map display widget that currently has
  125. the focus (as indicted by mdidx).
  126. """
  127. # map display window available ?
  128. try:
  129. curr_disp = self.parent.curr_page.maptree.mapdisplay
  130. self.Map = curr_disp.GetRender()
  131. except:
  132. curr_disp = None
  133. if len(self.GetListOfCmdThreads()) > 0:
  134. # only one running command enabled (per GMConsole instance)
  135. busy = wx.BusyInfo(message=_("Unable to run the command, another command is running..."),
  136. parent=self)
  137. wx.Yield()
  138. time.sleep(3)
  139. busy.Destroy()
  140. return None
  141. # command given as a string ?
  142. try:
  143. cmdlist = command.strip().split(' ')
  144. except:
  145. cmdlist = command
  146. if cmdlist[0] in globalvar.grassCmd['all']:
  147. # send GRASS command without arguments to GUI command interface
  148. # except display commands (they are handled differently)
  149. if cmdlist[0][0:2] == "d.":
  150. #
  151. # display GRASS commands
  152. #
  153. try:
  154. layertype = {'d.rast' : 'raster',
  155. 'd.rgb' : 'rgb',
  156. 'd.his' : 'his',
  157. 'd.shaded' : 'shaded',
  158. 'd.legend' : 'rastleg',
  159. 'd.rast.arrow' : 'rastarrow',
  160. 'd.rast.num' : 'rastnum',
  161. 'd.vect' : 'vector',
  162. 'd.vect.thematic': 'thememap',
  163. 'd.vect.chart' : 'themechart',
  164. 'd.grid' : 'grid',
  165. 'd.geodesic' : 'geodesic',
  166. 'd.rhumbline' : 'rhumb',
  167. 'd.labels' : 'labels'}[cmdlist[0]]
  168. except KeyError:
  169. wx.MessageBox(message=_("Command '%s' not yet implemented.") % cmdlist[0])
  170. return None
  171. # add layer into layer tree
  172. self.parent.curr_page.maptree.AddLayer(ltype=layertype,
  173. lcmd=cmdlist)
  174. else:
  175. #
  176. # other GRASS commands (r|v|g|...)
  177. #
  178. if hasattr(self.parent, "curr_page"):
  179. # change notebook page only for Layer Manager
  180. if self.parent.notebook.GetSelection() != 1:
  181. self.parent.notebook.SetSelection(1)
  182. # activate computational region (set with g.region)
  183. # for all non-display commands.
  184. tmpreg = os.getenv("GRASS_REGION")
  185. os.unsetenv("GRASS_REGION")
  186. if len(cmdlist) == 1:
  187. import menuform
  188. # process GRASS command without argument
  189. menuform.GUI().ParseCommand(cmdlist, parentframe=self)
  190. else:
  191. # process GRASS command with argument
  192. cmdPID = len(self.cmdThreads.keys())+1
  193. self.WriteCmdLog('%s' % ' '.join(cmdlist), pid=cmdPID)
  194. grassCmd = gcmd.Command(cmdlist, wait=False,
  195. stdout=self.cmd_stdout,
  196. stderr=self.cmd_stderr)
  197. self.cmdThreads[grassCmd.cmdThread] = { 'cmdPID' : cmdPID }
  198. return grassCmd
  199. # deactivate computational region and return to display settings
  200. if tmpreg:
  201. os.environ["GRASS_REGION"] = tmpreg
  202. else:
  203. # Send any other command to the shell. Send output to
  204. # console output window
  205. if hasattr(self.parent, "curr_page"):
  206. # change notebook page only for Layer Manager
  207. if self.parent.notebook.GetSelection() != 1:
  208. self.parent.notebook.SetSelection(1)
  209. # if command is not a GRASS command, treat it like a shell command
  210. try:
  211. generalCmd = gcmd.Command(cmdlist,
  212. stdout=self.cmd_stdout,
  213. stderr=self.cmd_stderr)
  214. except gcmd.CmdError, e:
  215. print >> sys.stderr, e
  216. return None
  217. def ClearHistory(self, event):
  218. """Clear history of commands"""
  219. self.cmd_output.ClearAll()
  220. self.console_progressbar.SetValue(0)
  221. def SaveHistory(self, event):
  222. """Save history of commands"""
  223. self.history = self.cmd_output.GetSelectedText()
  224. if self.history == '':
  225. self.history = self.cmd_output.GetText()
  226. # add newline if needed
  227. if len(self.history) > 0 and self.history[-1] != os.linesep:
  228. self.history += os.linesep
  229. wildcard = "Text file (*.txt)|*.txt"
  230. dlg = wx.FileDialog(
  231. self, message=_("Save file as..."), defaultDir=os.getcwd(),
  232. defaultFile="grass_cmd_history.txt", wildcard=wildcard,
  233. style=wx.SAVE|wx.FD_OVERWRITE_PROMPT)
  234. # Show the dialog and retrieve the user response. If it is the OK response,
  235. # process the data.
  236. if dlg.ShowModal() == wx.ID_OK:
  237. path = dlg.GetPath()
  238. output = open(path, "w")
  239. output.write(self.history)
  240. output.close()
  241. dlg.Destroy()
  242. def GetListOfCmdThreads(self, onlyAlive=True):
  243. """Return list of command threads)"""
  244. list = []
  245. for t in self.cmdThreads.keys():
  246. Debug.msg (4, "GMConsole.GetListOfCmdThreads(): name=%s, alive=%s" %
  247. (t.getName(), t.isAlive()))
  248. if onlyAlive and not t.isAlive():
  249. continue
  250. list.append(t)
  251. return list
  252. def OnResult(self, event):
  253. """Show result status"""
  254. if event.cmdThread.aborted:
  255. # Thread aborted (using our convention of None return)
  256. self.WriteLog(_('Please note that the data are left in incosistent stage '
  257. 'and can be corrupted'), self.cmd_output.StyleWarning)
  258. self.WriteCmdLog(_('Command aborted'),
  259. pid=self.cmdThreads[event.cmdThread]['cmdPID'])
  260. else:
  261. try:
  262. # Process results here
  263. self.WriteCmdLog(_('Command finished (%d sec)') % (time.time() - event.cmdThread.startTime),
  264. pid=self.cmdThreads[event.cmdThread]['cmdPID'])
  265. except KeyError:
  266. # stopped deamon
  267. pass
  268. self.console_progressbar.SetValue(0) # reset progress bar on '0%'
  269. # updated command dialog
  270. if hasattr(self.parent.parent, "btn_run"):
  271. dialog = self.parent.parent
  272. if hasattr(self.parent.parent, "btn_abort"):
  273. dialog.btn_abort.Enable(False)
  274. if hasattr(self.parent.parent, "btn_cancel"):
  275. dialog.btn_cancel.Enable(True)
  276. if hasattr(self.parent.parent, "btn_clipboard"):
  277. dialog.btn_clipboard.Enable(True)
  278. if hasattr(self.parent.parent, "btn_help"):
  279. dialog.btn_help.Enable(True)
  280. dialog.btn_run.Enable(True)
  281. if dialog.get_dcmd is None and \
  282. dialog.closebox.IsChecked():
  283. time.sleep(1)
  284. dialog.Close()
  285. class GMStdout:
  286. """GMConsole standard output
  287. Based on FrameOutErr.py
  288. Name: FrameOutErr.py
  289. Purpose: Redirecting stdout / stderr
  290. Author: Jean-Michel Fauth, Switzerland
  291. Copyright: (c) 2005-2007 Jean-Michel Fauth
  292. Licence: GPL
  293. """
  294. def __init__(self, gmstc):
  295. self.gmstc = gmstc
  296. def write(self, s):
  297. if len(s) == 0:
  298. return
  299. s = s.replace('\n', os.linesep)
  300. for line in s.split(os.linesep):
  301. p1 = self.gmstc.GetCurrentPos() # get caret position
  302. self.gmstc.AddTextWrapped(line, wrap=None) # no wrapping && adds os.linesep
  303. # self.gmstc.EnsureCaretVisible()
  304. p2 = self.gmstc.GetCurrentPos()
  305. self.gmstc.StartStyling(p1, 0xff)
  306. self.gmstc.SetStyling(p2 - p1 + 1, self.gmstc.StyleOutput)
  307. class GMStderr:
  308. """GMConsole standard error output
  309. Based on FrameOutErr.py
  310. Name: FrameOutErr.py
  311. Purpose: Redirecting stdout / stderr
  312. Author: Jean-Michel Fauth, Switzerland
  313. Copyright: (c) 2005-2007 Jean-Michel Fauth
  314. Licence: GPL
  315. """
  316. def __init__(self, gmstc, gmgauge, notebook, pageid):
  317. self.gmstc = gmstc
  318. self.gmgauge = gmgauge
  319. self.notebook = notebook
  320. self.pageid = pageid
  321. def write(self, s):
  322. if self.pageid > -1:
  323. # swith notebook page to 'command output'
  324. if self.notebook.GetSelection() != self.pageid:
  325. self.notebook.SetSelection(self.pageid)
  326. s = s.replace('\n', os.linesep)
  327. # remove/replace escape sequences '\b' or '\r' from stream
  328. s = s.replace('\b', '').replace('\r', '%s' % os.linesep)
  329. type = ''
  330. message = ''
  331. printMessage = False
  332. for line in s.split(os.linesep):
  333. if len(line) == 0:
  334. continue
  335. if 'GRASS_INFO_PERCENT' in line:
  336. # 'GRASS_INFO_PERCENT: 10' -> value=10
  337. value = int(line.rsplit(':', 1)[1].strip())
  338. if value >= 0 and value < 100:
  339. self.gmgauge.SetValue(value)
  340. else:
  341. self.gmgauge.SetValue(0) # reset progress bar on '0%'
  342. elif 'GRASS_INFO_MESSAGE' in line:
  343. type = 'message'
  344. if len(message) > 0:
  345. message += os.linesep
  346. message += line.split(':', 1)[1].strip()
  347. elif 'GRASS_INFO_WARNING' in line:
  348. type = 'warning'
  349. if len(message) > 0:
  350. message += os.linesep
  351. message += line.split(':', 1)[1].strip()
  352. elif 'GRASS_INFO_ERROR' in line:
  353. type = 'error'
  354. if len(message) > 0:
  355. message += os.linesep
  356. message += line.split(':', 1)[1].strip()
  357. elif 'GRASS_INFO_END' in line:
  358. printMessage = True
  359. elif not type:
  360. if len(line) > 0:
  361. p1 = self.gmstc.GetCurrentPos()
  362. self.gmstc.AddTextWrapped(line, wrap=60) # wrap && add os.linesep
  363. # self.gmstc.EnsureCaretVisible()
  364. p2 = self.gmstc.GetCurrentPos()
  365. self.gmstc.StartStyling(p1, 0xff)
  366. self.gmstc.SetStyling(p2 - p1 + 1, self.gmstc.StyleUnknown)
  367. elif len(line) > 0:
  368. message += os.linesep + line.strip()
  369. if printMessage and len(message) > 0:
  370. p1 = self.gmstc.GetCurrentPos()
  371. if type == 'warning':
  372. message = 'WARNING: ' + message
  373. elif type == 'error':
  374. message = 'ERROR: ' + message
  375. if os.linesep not in message:
  376. self.gmstc.AddTextWrapped(message, wrap=60) #wrap && add os.linesep
  377. else:
  378. self.gmstc.AddText(message + os.linesep)
  379. # self.gmstc.EnsureCaretVisible()
  380. p2 = self.gmstc.GetCurrentPos()
  381. self.gmstc.StartStyling(p1, 0xff)
  382. if type == 'error':
  383. self.gmstc.SetStyling(p2 - p1 + 1, self.gmstc.StyleError)
  384. elif type == 'warning':
  385. self.gmstc.SetStyling(p2 - p1 + 1, self.gmstc.StyleWarning)
  386. elif type == 'message':
  387. self.gmstc.SetStyling(p2 - p1 + 1, self.gmstc.StyleMessage)
  388. type = ''
  389. message = ''
  390. class GMStc(wx.stc.StyledTextCtrl):
  391. """Styled GMConsole
  392. Based on FrameOutErr.py
  393. Name: FrameOutErr.py
  394. Purpose: Redirecting stdout / stderr
  395. Author: Jean-Michel Fauth, Switzerland
  396. Copyright: (c) 2005-2007 Jean-Michel Fauth
  397. Licence: GPL
  398. """
  399. def __init__(self, parent, id, margin=False, wrap=None):
  400. wx.stc.StyledTextCtrl.__init__(self, parent, id)
  401. self.parent = parent
  402. self.wrap = wrap
  403. #
  404. # styles
  405. #
  406. self.StyleDefault = 0
  407. self.StyleDefaultSpec = "face:Courier New,size:10,fore:#000000,back:#FFFFFF"
  408. self.StyleCommand = 1
  409. self.StyleCommandSpec = "face:Courier New,size:10,fore:#000000,back:#bcbcbc"
  410. self.StyleOutput = 2
  411. self.StyleOutputSpec = "face:Courier New,size:10,fore:#000000,back:#FFFFFF"
  412. # fatal error
  413. self.StyleError = 3
  414. self.StyleErrorSpec = "face:Courier New,size:10,fore:#7F0000,back:#FFFFFF"
  415. # warning
  416. self.StyleWarning = 4
  417. self.StyleWarningSpec = "face:Courier New,size:10,fore:#0000FF,back:#FFFFFF"
  418. # message
  419. self.StyleMessage = 5
  420. self.StyleMessageSpec = "face:Courier New,size:10,fore:#000000,back:#FFFFFF"
  421. # unknown
  422. self.StyleUnknown = 6
  423. self.StyleUnknownSpec = "face:Courier New,size:10,fore:#7F0000,back:#FFFFFF"
  424. # default and clear => init
  425. self.StyleSetSpec(wx.stc.STC_STYLE_DEFAULT, self.StyleDefaultSpec)
  426. self.StyleClearAll()
  427. self.StyleSetSpec(self.StyleCommand, self.StyleCommandSpec)
  428. self.StyleSetSpec(self.StyleOutput, self.StyleOutputSpec)
  429. self.StyleSetSpec(self.StyleError, self.StyleErrorSpec)
  430. self.StyleSetSpec(self.StyleWarning, self.StyleWarningSpec)
  431. self.StyleSetSpec(self.StyleMessage, self.StyleMessageSpec)
  432. self.StyleSetSpec(self.StyleUnknown, self.StyleUnknownSpec)
  433. #
  434. # line margins
  435. #
  436. # TODO print number only from cmdlog
  437. self.SetMarginWidth(1, 0)
  438. self.SetMarginWidth(2, 0)
  439. if margin:
  440. self.SetMarginType(0, wx.stc.STC_MARGIN_NUMBER)
  441. self.SetMarginWidth(0, 30)
  442. else:
  443. self.SetMarginWidth(0, 0)
  444. #
  445. # miscellaneous
  446. #
  447. self.SetViewWhiteSpace(False)
  448. self.SetTabWidth(4)
  449. self.SetUseTabs(False)
  450. self.UsePopUp(True)
  451. self.SetSelBackground(True, "#FFFF00")
  452. self.SetUseHorizontalScrollBar(True)
  453. #
  454. # bindins
  455. #
  456. self.Bind(wx.EVT_WINDOW_DESTROY, self.OnDestroy)
  457. def OnDestroy(self, evt):
  458. """The clipboard contents can be preserved after
  459. the app has exited"""
  460. wx.TheClipboard.Flush()
  461. evt.Skip()
  462. def AddTextWrapped(self, str, wrap=None):
  463. """Add string to text area.
  464. String is wrapped and linesep is also added to the end
  465. of the string"""
  466. if wrap is None and self.wrap:
  467. wrap = self.wrap
  468. if wrap is not None:
  469. str = textwrap.fill(str, wrap) + os.linesep
  470. else:
  471. str += os.linesep
  472. self.AddText(str)
  473. def SetWrap(self, wrap):
  474. """Set wrapping value
  475. @param wrap wrapping value
  476. @return current wrapping value
  477. """
  478. if wrap > 0:
  479. self.wrap = wrap
  480. return self.wrap