goutput.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571
  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. self.cmd_stderr = GMStderr(self.cmd_output,
  50. self.console_progressbar,
  51. self.parent.notebook,
  52. pageid)
  53. # buttons
  54. self.console_clear = wx.Button(parent=self, id=wx.ID_CLEAR)
  55. self.console_save = wx.Button(parent=self, id=wx.ID_SAVE)
  56. self.Bind(wx.EVT_BUTTON, self.ClearHistory, self.console_clear)
  57. self.Bind(wx.EVT_BUTTON, self.SaveHistory, self.console_save)
  58. # output control layout
  59. boxsizer1 = wx.BoxSizer(wx.VERTICAL)
  60. gridsizer1 = wx.GridSizer(rows=1, cols=2, vgap=0, hgap=0)
  61. boxsizer1.Add(item=self.cmd_output, proportion=1,
  62. flag=wx.EXPAND | wx.ADJUST_MINSIZE, border=0)
  63. gridsizer1.Add(item=self.console_clear, proportion=0,
  64. flag=wx.ALIGN_CENTER_HORIZONTAL | wx.ADJUST_MINSIZE, border=0)
  65. gridsizer1.Add(item=self.console_save, proportion=0,
  66. flag=wx.ALIGN_CENTER_HORIZONTAL | wx.ADJUST_MINSIZE, border=0)
  67. boxsizer1.Add(item=gridsizer1, proportion=0,
  68. flag=wx.EXPAND | wx.ALIGN_CENTRE_VERTICAL | wx.TOP | wx.BOTTOM,
  69. border=5)
  70. boxsizer1.Add(item=self.console_progressbar, proportion=0,
  71. flag=wx.EXPAND | wx.ADJUST_MINSIZE | wx.LEFT | wx.RIGHT | wx.BOTTOM, border=5)
  72. boxsizer1.Fit(self)
  73. boxsizer1.SetSizeHints(self)
  74. # set up event handler for any command thread results
  75. gcmd.EVT_RESULT(self, self.OnResult)
  76. # layout
  77. self.SetAutoLayout(True)
  78. self.SetSizer(boxsizer1)
  79. def Redirect(self):
  80. """Redirect stderr
  81. @return True redirected
  82. @return False failed
  83. """
  84. if Debug.get_level() == 0:
  85. # don't redirect when debugging is enabled
  86. sys.stdout = self.cmd_stdout
  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 or s == '\n':
  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(object):
  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. self.type = ''
  321. self.message = ''
  322. self.printMessage = False
  323. def write(self, s):
  324. if self.pageid > -1:
  325. # swith notebook page to 'command output'
  326. if self.notebook.GetSelection() != self.pageid:
  327. self.notebook.SetSelection(self.pageid)
  328. s = s.replace('\n', os.linesep)
  329. # remove/replace escape sequences '\b' or '\r' from stream
  330. s = s.replace('\b', '').replace('\r', '%s' % os.linesep)
  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. self.type = 'message'
  343. self.message = line.split(':', 1)[1].strip()
  344. elif 'GRASS_INFO_WARNING' in line:
  345. self.type = 'warning'
  346. self.message = line.split(':', 1)[1].strip()
  347. elif 'GRASS_INFO_ERROR' in line:
  348. self.type = 'error'
  349. self.message = line.split(':', 1)[1].strip()
  350. elif 'GRASS_INFO_END' in line:
  351. self.printMessage = True
  352. elif not self.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. self.message += line.strip() + os.linesep
  362. if self.printMessage and len(self.message) > 0:
  363. p1 = self.gmstc.GetCurrentPos()
  364. if self.type == 'warning':
  365. self.message = 'WARNING: ' + self.message
  366. elif self.type == 'error':
  367. self.message = 'ERROR: ' + self.message
  368. if os.linesep not in self.message:
  369. self.gmstc.AddTextWrapped(self.message, wrap=60) #wrap && add os.linesep
  370. else:
  371. self.gmstc.AddText(self.message)
  372. self.gmstc.EnsureCaretVisible()
  373. p2 = self.gmstc.GetCurrentPos()
  374. self.gmstc.StartStyling(p1, 0xff)
  375. if self.type == 'error':
  376. self.gmstc.SetStyling(p2 - p1 + 1, self.gmstc.StyleError)
  377. elif self.type == 'warning':
  378. self.gmstc.SetStyling(p2 - p1 + 1, self.gmstc.StyleWarning)
  379. elif self.type == 'self.message':
  380. self.gmstc.SetStyling(p2 - p1 + 1, self.gmstc.StyleSelf.Message)
  381. self.type = ''
  382. self.message = ''
  383. self.printMessage = False
  384. class GMStc(wx.stc.StyledTextCtrl):
  385. """Styled GMConsole
  386. Based on FrameOutErr.py
  387. Name: FrameOutErr.py
  388. Purpose: Redirecting stdout / stderr
  389. Author: Jean-Michel Fauth, Switzerland
  390. Copyright: (c) 2005-2007 Jean-Michel Fauth
  391. Licence: GPL
  392. """
  393. def __init__(self, parent, id, margin=False, wrap=None):
  394. wx.stc.StyledTextCtrl.__init__(self, parent, id)
  395. self.parent = parent
  396. self.wrap = wrap
  397. #
  398. # styles
  399. #
  400. self.StyleDefault = 0
  401. self.StyleDefaultSpec = "face:Courier New,size:10,fore:#000000,back:#FFFFFF"
  402. self.StyleCommand = 1
  403. self.StyleCommandSpec = "face:Courier New,size:10,fore:#000000,back:#bcbcbc"
  404. self.StyleOutput = 2
  405. self.StyleOutputSpec = "face:Courier New,size:10,fore:#000000,back:#FFFFFF"
  406. # fatal error
  407. self.StyleError = 3
  408. self.StyleErrorSpec = "face:Courier New,size:10,fore:#7F0000,back:#FFFFFF"
  409. # warning
  410. self.StyleWarning = 4
  411. self.StyleWarningSpec = "face:Courier New,size:10,fore:#0000FF,back:#FFFFFF"
  412. # message
  413. self.StyleMessage = 5
  414. self.StyleMessageSpec = "face:Courier New,size:10,fore:#000000,back:#FFFFFF"
  415. # unknown
  416. self.StyleUnknown = 6
  417. self.StyleUnknownSpec = "face:Courier New,size:10,fore:#7F0000,back:#FFFFFF"
  418. # default and clear => init
  419. self.StyleSetSpec(wx.stc.STC_STYLE_DEFAULT, self.StyleDefaultSpec)
  420. self.StyleClearAll()
  421. self.StyleSetSpec(self.StyleCommand, self.StyleCommandSpec)
  422. self.StyleSetSpec(self.StyleOutput, self.StyleOutputSpec)
  423. self.StyleSetSpec(self.StyleError, self.StyleErrorSpec)
  424. self.StyleSetSpec(self.StyleWarning, self.StyleWarningSpec)
  425. self.StyleSetSpec(self.StyleMessage, self.StyleMessageSpec)
  426. self.StyleSetSpec(self.StyleUnknown, self.StyleUnknownSpec)
  427. #
  428. # line margins
  429. #
  430. # TODO print number only from cmdlog
  431. self.SetMarginWidth(1, 0)
  432. self.SetMarginWidth(2, 0)
  433. if margin:
  434. self.SetMarginType(0, wx.stc.STC_MARGIN_NUMBER)
  435. self.SetMarginWidth(0, 30)
  436. else:
  437. self.SetMarginWidth(0, 0)
  438. #
  439. # miscellaneous
  440. #
  441. self.SetViewWhiteSpace(False)
  442. self.SetTabWidth(4)
  443. self.SetUseTabs(False)
  444. self.UsePopUp(True)
  445. self.SetSelBackground(True, "#FFFF00")
  446. self.SetUseHorizontalScrollBar(True)
  447. #
  448. # bindins
  449. #
  450. self.Bind(wx.EVT_WINDOW_DESTROY, self.OnDestroy)
  451. def OnDestroy(self, evt):
  452. """The clipboard contents can be preserved after
  453. the app has exited"""
  454. wx.TheClipboard.Flush()
  455. evt.Skip()
  456. def AddTextWrapped(self, str, wrap=None):
  457. """Add string to text area.
  458. String is wrapped and linesep is also added to the end
  459. of the string"""
  460. if wrap is None and self.wrap:
  461. wrap = self.wrap
  462. if wrap is not None:
  463. str = textwrap.fill(str, wrap) + os.linesep
  464. else:
  465. str += os.linesep
  466. self.AddText(str)
  467. def SetWrap(self, wrap):
  468. """Set wrapping value
  469. @param wrap wrapping value
  470. @return current wrapping value
  471. """
  472. if wrap > 0:
  473. self.wrap = wrap
  474. return self.wrap