goutput.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564
  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. dialog.btn_run.Enable(True)
  274. if dialog.get_dcmd is None and \
  275. dialog.closebox.IsChecked():
  276. time.sleep(1)
  277. dialog.Close()
  278. class GMStdout:
  279. """GMConsole standard output
  280. Based on FrameOutErr.py
  281. Name: FrameOutErr.py
  282. Purpose: Redirecting stdout / stderr
  283. Author: Jean-Michel Fauth, Switzerland
  284. Copyright: (c) 2005-2007 Jean-Michel Fauth
  285. Licence: GPL
  286. """
  287. def __init__(self, gmstc):
  288. self.gmstc = gmstc
  289. def write(self, s):
  290. if len(s) == 0:
  291. return
  292. s = s.replace('\n', os.linesep)
  293. for line in s.split(os.linesep):
  294. p1 = self.gmstc.GetCurrentPos() # get caret position
  295. self.gmstc.AddTextWrapped(line, wrap=None) # no wrapping && adds os.linesep
  296. # self.gmstc.EnsureCaretVisible()
  297. p2 = self.gmstc.GetCurrentPos()
  298. self.gmstc.StartStyling(p1, 0xff)
  299. self.gmstc.SetStyling(p2 - p1 + 1, self.gmstc.StyleOutput)
  300. class GMStderr:
  301. """GMConsole standard error output
  302. Based on FrameOutErr.py
  303. Name: FrameOutErr.py
  304. Purpose: Redirecting stdout / stderr
  305. Author: Jean-Michel Fauth, Switzerland
  306. Copyright: (c) 2005-2007 Jean-Michel Fauth
  307. Licence: GPL
  308. """
  309. def __init__(self, gmstc, gmgauge, notebook, pageid):
  310. self.gmstc = gmstc
  311. self.gmgauge = gmgauge
  312. self.notebook = notebook
  313. self.pageid = pageid
  314. def write(self, s):
  315. if self.pageid > -1:
  316. # swith notebook page to 'command output'
  317. if self.notebook.GetSelection() != self.pageid:
  318. self.notebook.SetSelection(self.pageid)
  319. s = s.replace('\n', os.linesep)
  320. # remove/replace escape sequences '\b' or '\r' from stream
  321. s = s.replace('\b', '').replace('\r', '%s' % os.linesep)
  322. type = ''
  323. message = ''
  324. printMessage = False
  325. for line in s.split(os.linesep):
  326. if len(line) == 0:
  327. continue
  328. if 'GRASS_INFO_PERCENT' in line:
  329. # 'GRASS_INFO_PERCENT: 10' -> value=10
  330. value = int(line.rsplit(':', 1)[1].strip())
  331. if value >= 0 and value < 100:
  332. self.gmgauge.SetValue(value)
  333. else:
  334. self.gmgauge.SetValue(0) # reset progress bar on '0%'
  335. elif 'GRASS_INFO_MESSAGE' in line:
  336. type = 'message'
  337. if len(message) > 0:
  338. message += os.linesep
  339. message += line.split(':', 1)[1].strip()
  340. elif 'GRASS_INFO_WARNING' in line:
  341. type = 'warning'
  342. if len(message) > 0:
  343. message += os.linesep
  344. message += line.split(':', 1)[1].strip()
  345. elif 'GRASS_INFO_ERROR' in line:
  346. type = 'error'
  347. if len(message) > 0:
  348. message += os.linesep
  349. message += line.split(':', 1)[1].strip()
  350. elif 'GRASS_INFO_END' in line:
  351. printMessage = True
  352. elif not type:
  353. if len(line) > 0:
  354. p1 = self.gmstc.GetCurrentPos()
  355. self.gmstc.AddTextWrapped(line, wrap=60) # wrap && add os.linesep
  356. # self.gmstc.EnsureCaretVisible()
  357. p2 = self.gmstc.GetCurrentPos()
  358. self.gmstc.StartStyling(p1, 0xff)
  359. self.gmstc.SetStyling(p2 - p1 + 1, self.gmstc.StyleUnknown)
  360. elif len(line) > 0:
  361. message += os.linesep + line.strip()
  362. if printMessage and len(message) > 0:
  363. p1 = self.gmstc.GetCurrentPos()
  364. if type == 'warning':
  365. message = 'WARNING: ' + message
  366. elif type == 'error':
  367. message = 'ERROR: ' + message
  368. if os.linesep not in message:
  369. self.gmstc.AddTextWrapped(message, wrap=60) #wrap && add os.linesep
  370. else:
  371. self.gmstc.AddText(message + os.linesep)
  372. # self.gmstc.EnsureCaretVisible()
  373. p2 = self.gmstc.GetCurrentPos()
  374. self.gmstc.StartStyling(p1, 0xff)
  375. if type == 'error':
  376. self.gmstc.SetStyling(p2 - p1 + 1, self.gmstc.StyleError)
  377. elif type == 'warning':
  378. self.gmstc.SetStyling(p2 - p1 + 1, self.gmstc.StyleWarning)
  379. elif type == 'message':
  380. self.gmstc.SetStyling(p2 - p1 + 1, self.gmstc.StyleMessage)
  381. type = ''
  382. message = ''
  383. class GMStc(wx.stc.StyledTextCtrl):
  384. """Styled GMConsole
  385. Based on FrameOutErr.py
  386. Name: FrameOutErr.py
  387. Purpose: Redirecting stdout / stderr
  388. Author: Jean-Michel Fauth, Switzerland
  389. Copyright: (c) 2005-2007 Jean-Michel Fauth
  390. Licence: GPL
  391. """
  392. def __init__(self, parent, id, margin=False, wrap=None):
  393. wx.stc.StyledTextCtrl.__init__(self, parent, id)
  394. self.parent = parent
  395. self.wrap = wrap
  396. #
  397. # styles
  398. #
  399. self.StyleDefault = 0
  400. self.StyleDefaultSpec = "face:Courier New,size:10,fore:#000000,back:#FFFFFF"
  401. self.StyleCommand = 1
  402. self.StyleCommandSpec = "face:Courier New,size:10,fore:#000000,back:#bcbcbc"
  403. self.StyleOutput = 2
  404. self.StyleOutputSpec = "face:Courier New,size:10,fore:#000000,back:#FFFFFF"
  405. # fatal error
  406. self.StyleError = 3
  407. self.StyleErrorSpec = "face:Courier New,size:10,fore:#7F0000,back:#FFFFFF"
  408. # warning
  409. self.StyleWarning = 4
  410. self.StyleWarningSpec = "face:Courier New,size:10,fore:#0000FF,back:#FFFFFF"
  411. # message
  412. self.StyleMessage = 5
  413. self.StyleMessageSpec = "face:Courier New,size:10,fore:#000000,back:#FFFFFF"
  414. # unknown
  415. self.StyleUnknown = 6
  416. self.StyleUnknownSpec = "face:Courier New,size:10,fore:#7F0000,back:#FFFFFF"
  417. # default and clear => init
  418. self.StyleSetSpec(wx.stc.STC_STYLE_DEFAULT, self.StyleDefaultSpec)
  419. self.StyleClearAll()
  420. self.StyleSetSpec(self.StyleCommand, self.StyleCommandSpec)
  421. self.StyleSetSpec(self.StyleOutput, self.StyleOutputSpec)
  422. self.StyleSetSpec(self.StyleError, self.StyleErrorSpec)
  423. self.StyleSetSpec(self.StyleWarning, self.StyleWarningSpec)
  424. self.StyleSetSpec(self.StyleMessage, self.StyleMessageSpec)
  425. self.StyleSetSpec(self.StyleUnknown, self.StyleUnknownSpec)
  426. #
  427. # line margins
  428. #
  429. # TODO print number only from cmdlog
  430. self.SetMarginWidth(1, 0)
  431. self.SetMarginWidth(2, 0)
  432. if margin:
  433. self.SetMarginType(0, wx.stc.STC_MARGIN_NUMBER)
  434. self.SetMarginWidth(0, 30)
  435. else:
  436. self.SetMarginWidth(0, 0)
  437. #
  438. # miscellaneous
  439. #
  440. self.SetViewWhiteSpace(False)
  441. self.SetTabWidth(4)
  442. self.SetUseTabs(False)
  443. self.UsePopUp(True)
  444. self.SetSelBackground(True, "#FFFF00")
  445. self.SetUseHorizontalScrollBar(True)
  446. #
  447. # bindins
  448. #
  449. self.Bind(wx.EVT_WINDOW_DESTROY, self.OnDestroy)
  450. def OnDestroy(self, evt):
  451. """The clipboard contents can be preserved after
  452. the app has exited"""
  453. wx.TheClipboard.Flush()
  454. evt.Skip()
  455. def AddTextWrapped(self, str, wrap=None):
  456. """Add string to text area.
  457. String is wrapped and linesep is also added to the end
  458. of the string"""
  459. if wrap is None and self.wrap:
  460. wrap = self.wrap
  461. if wrap is not None:
  462. str = textwrap.fill(str, wrap) + os.linesep
  463. else:
  464. str += os.linesep
  465. self.AddText(str)
  466. def SetWrap(self, wrap):
  467. """Set wrapping value
  468. @param wrap wrapping value
  469. @return current wrapping value
  470. """
  471. if wrap > 0:
  472. self.wrap = wrap
  473. return self.wrap