goutput.py 20 KB

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