goutput.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845
  1. """
  2. @package gui_core.goutput
  3. @brief Command output widgets
  4. Classes:
  5. - goutput::GConsoleWindow
  6. - goutput::GStc
  7. - goutput::GConsoleFrame
  8. (C) 2007-2012 by the GRASS Development Team
  9. This program is free software under the GNU General Public License
  10. (>=v2). Read the file COPYING that comes with GRASS for details.
  11. @author Michael Barton (Arizona State University)
  12. @author Martin Landa <landa.martin gmail.com>
  13. @author Vaclav Petras <wenzeslaus gmail.com> (refactoring)
  14. @author Anna Kratochvilova <kratochanna gmail.com> (refactoring)
  15. """
  16. import os
  17. import textwrap
  18. import wx
  19. from wx import stc
  20. from grass.pydispatch.signal import Signal
  21. # needed just for testing
  22. if __name__ == '__main__':
  23. from grass.script.setup import set_gui_path
  24. set_gui_path()
  25. from core.gcmd import GError, EncodeString
  26. from core.gconsole import GConsole, \
  27. EVT_CMD_OUTPUT, EVT_CMD_PROGRESS, EVT_CMD_RUN, EVT_CMD_DONE, \
  28. Notification
  29. from core.globalvar import CheckWxVersion, wxPythonPhoenix
  30. from gui_core.prompt import GPromptSTC
  31. from gui_core.wrap import Button, ToggleButton, StaticText, \
  32. StaticBox
  33. from core.settings import UserSettings
  34. from gui_core.widgets import SearchModuleWidget
  35. GC_EMPTY = 0
  36. GC_SEARCH = 1
  37. GC_PROMPT = 2
  38. class GConsoleWindow(wx.SplitterWindow):
  39. """Create and manage output console for commands run by GUI.
  40. """
  41. def __init__(self, parent, gconsole, menuModel=None, margin=False,
  42. style=wx.TAB_TRAVERSAL | wx.FULL_REPAINT_ON_RESIZE,
  43. gcstyle=GC_EMPTY,
  44. **kwargs):
  45. """
  46. :param parent: gui parent
  47. :param gconsole: console logic
  48. :param menuModel: tree model of modules (from menu)
  49. :param margin: use margin in output pane (GStc)
  50. :param style: wx.SplitterWindow style
  51. :param gcstyle: GConsole style
  52. (GC_EMPTY, GC_PROMPT to show command prompt,
  53. GC_SEARCH to show search widget)
  54. """
  55. wx.SplitterWindow.__init__(
  56. self, parent, id=wx.ID_ANY, style=style, **kwargs)
  57. self.SetName("GConsole")
  58. self.panelOutput = wx.Panel(parent=self, id=wx.ID_ANY)
  59. self.panelProgress = wx.Panel(
  60. parent=self.panelOutput,
  61. id=wx.ID_ANY,
  62. name='progressPanel')
  63. self.panelPrompt = wx.Panel(parent=self, id=wx.ID_ANY)
  64. # initialize variables
  65. self.parent = parent # GMFrame | CmdPanel | ?
  66. self._gconsole = gconsole
  67. self._menuModel = menuModel
  68. self._gcstyle = gcstyle
  69. self.lineWidth = 80
  70. # signal which requests showing of a notification
  71. self.showNotification = Signal("GConsoleWindow.showNotification")
  72. # signal emitted when text appears in the console
  73. # parameter 'notification' suggests form of notification (according to
  74. # core.giface.Notification)
  75. self.contentChanged = Signal("GConsoleWindow.contentChanged")
  76. # progress bar
  77. self.progressbar = wx.Gauge(parent=self.panelProgress, id=wx.ID_ANY,
  78. range=100, pos=(110, 50), size=(-1, 25),
  79. style=wx.GA_HORIZONTAL)
  80. self._gconsole.Bind(EVT_CMD_PROGRESS, self.OnCmdProgress)
  81. self._gconsole.Bind(EVT_CMD_OUTPUT, self.OnCmdOutput)
  82. self._gconsole.Bind(EVT_CMD_RUN, self.OnCmdRun)
  83. self._gconsole.Bind(EVT_CMD_DONE, self.OnCmdDone)
  84. self._gconsole.writeLog.connect(self.WriteLog)
  85. self._gconsole.writeCmdLog.connect(self.WriteCmdLog)
  86. self._gconsole.writeWarning.connect(self.WriteWarning)
  87. self._gconsole.writeError.connect(self.WriteError)
  88. # text control for command output
  89. self.cmdOutput = GStc(
  90. parent=self.panelOutput,
  91. id=wx.ID_ANY,
  92. margin=margin,
  93. wrap=None)
  94. # search & command prompt
  95. # move to the if below
  96. # search depends on cmd prompt
  97. self.cmdPrompt = GPromptSTC(parent=self, menuModel=self._menuModel)
  98. self.cmdPrompt.promptRunCmd.connect(lambda cmd:
  99. self._gconsole.RunCmd(command=cmd))
  100. self.cmdPrompt.showNotification.connect(self.showNotification)
  101. if not self._gcstyle & GC_PROMPT:
  102. self.cmdPrompt.Hide()
  103. if self._gcstyle & GC_SEARCH:
  104. self.infoCollapseLabelExp = _(
  105. "Click here to show search module engine")
  106. self.infoCollapseLabelCol = _(
  107. "Click here to hide search module engine")
  108. self.searchPane = wx.CollapsiblePane(
  109. parent=self.panelOutput, label=self.infoCollapseLabelExp,
  110. style=wx.CP_DEFAULT_STYLE | wx.CP_NO_TLW_RESIZE | wx.EXPAND)
  111. self.MakeSearchPaneContent(
  112. self.searchPane.GetPane(), self._menuModel)
  113. self.searchPane.Collapse(True)
  114. self.Bind(
  115. wx.EVT_COLLAPSIBLEPANE_CHANGED,
  116. self.OnSearchPaneChanged,
  117. self.searchPane)
  118. self.search.moduleSelected.connect(
  119. lambda name: self.cmdPrompt.SetTextAndFocus(name + ' '))
  120. else:
  121. self.search = None
  122. if self._gcstyle & GC_PROMPT:
  123. cmdLabel = _("Command prompt")
  124. self.outputBox = StaticBox(
  125. parent=self.panelOutput,
  126. id=wx.ID_ANY,
  127. label=" %s " %
  128. _("Output window"))
  129. self.cmdBox = StaticBox(parent=self.panelOutput, id=wx.ID_ANY,
  130. label=" %s " % cmdLabel)
  131. # buttons
  132. self.btnOutputClear = Button(
  133. parent=self.panelOutput, id=wx.ID_CLEAR)
  134. self.btnOutputClear.SetToolTip(_("Clear output window content"))
  135. self.btnCmdClear = Button(parent=self.panelOutput, id=wx.ID_CLEAR)
  136. self.btnCmdClear.SetToolTip(_("Clear command prompt content"))
  137. self.btnOutputSave = Button(parent=self.panelOutput, id=wx.ID_SAVE)
  138. self.btnOutputSave.SetToolTip(
  139. _("Save output window content to the file"))
  140. self.btnCmdAbort = Button(parent=self.panelProgress, id=wx.ID_STOP)
  141. self.btnCmdAbort.SetToolTip(_("Abort running command"))
  142. self.btnCmdProtocol = ToggleButton(
  143. parent=self.panelOutput,
  144. id=wx.ID_ANY,
  145. label=_("&Log file"),
  146. size=self.btnCmdClear.GetSize())
  147. self.btnCmdProtocol.SetToolTip(_("Toggle to save list of executed commands into "
  148. "a file; content saved when switching off."))
  149. self.cmdFileProtocol = None
  150. if not self._gcstyle & GC_PROMPT:
  151. self.btnCmdClear.Hide()
  152. self.btnCmdProtocol.Hide()
  153. self.btnCmdClear.Bind(wx.EVT_BUTTON, self.cmdPrompt.OnCmdErase)
  154. self.btnOutputClear.Bind(wx.EVT_BUTTON, self.OnOutputClear)
  155. self.btnOutputSave.Bind(wx.EVT_BUTTON, self.OnOutputSave)
  156. self.btnCmdAbort.Bind(wx.EVT_BUTTON, self._gconsole.OnCmdAbort)
  157. self.btnCmdProtocol.Bind(wx.EVT_TOGGLEBUTTON, self.OnCmdProtocol)
  158. self._layout()
  159. def _layout(self):
  160. """Do layout"""
  161. self.outputSizer = wx.BoxSizer(wx.VERTICAL)
  162. progressSizer = wx.BoxSizer(wx.HORIZONTAL)
  163. btnSizer = wx.BoxSizer(wx.HORIZONTAL)
  164. if self._gcstyle & GC_PROMPT:
  165. outBtnSizer = wx.StaticBoxSizer(self.outputBox, wx.HORIZONTAL)
  166. cmdBtnSizer = wx.StaticBoxSizer(self.cmdBox, wx.HORIZONTAL)
  167. else:
  168. outBtnSizer = wx.BoxSizer(wx.HORIZONTAL)
  169. cmdBtnSizer = wx.BoxSizer(wx.HORIZONTAL)
  170. if self._gcstyle & GC_PROMPT:
  171. promptSizer = wx.BoxSizer(wx.VERTICAL)
  172. promptSizer.Add(self.cmdPrompt, proportion=1,
  173. flag=wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP,
  174. border=3)
  175. helpText = StaticText(
  176. self.panelPrompt, id=wx.ID_ANY,
  177. label="Press Tab to display command help, Ctrl+Space to autocomplete")
  178. helpText.SetForegroundColour(
  179. wx.SystemSettings.GetColour(
  180. wx.SYS_COLOUR_GRAYTEXT))
  181. promptSizer.Add(helpText,
  182. proportion=0, flag=wx.EXPAND | wx.LEFT, border=5)
  183. if self._gcstyle & GC_SEARCH:
  184. self.outputSizer.Add(self.searchPane, proportion=0,
  185. flag=wx.EXPAND | wx.ALL, border=3)
  186. self.outputSizer.Add(self.cmdOutput, proportion=1,
  187. flag=wx.EXPAND | wx.ALL, border=3)
  188. if self._gcstyle & GC_PROMPT:
  189. proportion = 1
  190. else:
  191. proportion = 0
  192. outBtnSizer.AddStretchSpacer()
  193. outBtnSizer.Add(
  194. self.btnOutputClear,
  195. proportion=proportion,
  196. flag=wx.ALIGN_LEFT | wx.LEFT | wx.RIGHT | wx.BOTTOM,
  197. border=5)
  198. outBtnSizer.Add(self.btnOutputSave, proportion=proportion,
  199. flag=wx.RIGHT | wx.BOTTOM, border=5)
  200. cmdBtnSizer.Add(
  201. self.btnCmdProtocol,
  202. proportion=1,
  203. flag=wx.ALIGN_CENTER | wx.ALIGN_CENTER_VERTICAL | wx.LEFT | wx.RIGHT | wx.BOTTOM,
  204. border=5)
  205. cmdBtnSizer.Add(self.btnCmdClear, proportion=1,
  206. flag=wx.ALIGN_CENTER | wx.RIGHT | wx.BOTTOM, border=5)
  207. progressSizer.Add(self.btnCmdAbort, proportion=0,
  208. flag=wx.ALL | wx.ALIGN_CENTER, border=5)
  209. progressSizer.Add(
  210. self.progressbar,
  211. proportion=1,
  212. flag=wx.ALIGN_CENTER | wx.RIGHT | wx.TOP | wx.BOTTOM,
  213. border=5)
  214. self.panelProgress.SetSizer(progressSizer)
  215. progressSizer.Fit(self.panelProgress)
  216. btnSizer.Add(outBtnSizer, proportion=1,
  217. flag=wx.ALL | wx.ALIGN_CENTER, border=5)
  218. btnSizer.Add(
  219. cmdBtnSizer,
  220. proportion=1,
  221. flag=wx.ALIGN_CENTER | wx.TOP | wx.BOTTOM | wx.RIGHT,
  222. border=5)
  223. self.outputSizer.Add(self.panelProgress, proportion=0,
  224. flag=wx.EXPAND)
  225. self.outputSizer.Add(btnSizer, proportion=0,
  226. flag=wx.EXPAND)
  227. self.outputSizer.Fit(self)
  228. self.outputSizer.SetSizeHints(self)
  229. self.panelOutput.SetSizer(self.outputSizer)
  230. self.outputSizer.FitInside(self.panelOutput)
  231. if self._gcstyle & GC_PROMPT:
  232. promptSizer.Fit(self)
  233. promptSizer.SetSizeHints(self)
  234. self.panelPrompt.SetSizer(promptSizer)
  235. # split window
  236. if self._gcstyle & GC_PROMPT:
  237. self.SplitHorizontally(self.panelOutput, self.panelPrompt, -50)
  238. else:
  239. self.SplitHorizontally(self.panelOutput, self.panelPrompt, -45)
  240. self.Unsplit()
  241. self.SetMinimumPaneSize(self.btnCmdClear.GetSize()[1] + 25)
  242. self.SetSashGravity(1.0)
  243. self.outputSizer.Hide(self.panelProgress)
  244. # layout
  245. self.SetAutoLayout(True)
  246. self.Layout()
  247. def MakeSearchPaneContent(self, pane, model):
  248. """Create search pane"""
  249. border = wx.BoxSizer(wx.VERTICAL)
  250. self.search = SearchModuleWidget(parent=pane,
  251. model=model)
  252. self.search.showNotification.connect(self.showNotification)
  253. border.Add(self.search, proportion=0,
  254. flag=wx.EXPAND | wx.ALL, border=1)
  255. pane.SetSizer(border)
  256. border.Fit(pane)
  257. def OnSearchPaneChanged(self, event):
  258. """Collapse search module box"""
  259. if self.searchPane.IsExpanded():
  260. self.searchPane.SetLabel(self.infoCollapseLabelCol)
  261. else:
  262. self.searchPane.SetLabel(self.infoCollapseLabelExp)
  263. self.panelOutput.Layout()
  264. self.panelOutput.SendSizeEvent()
  265. def GetPanel(self, prompt=True):
  266. """Get panel
  267. :param prompt: get prompt / output panel
  268. :return: wx.Panel reference
  269. """
  270. if prompt:
  271. return self.panelPrompt
  272. return self.panelOutput
  273. def WriteLog(self, text, style=None, wrap=None,
  274. notification=Notification.HIGHLIGHT):
  275. """Generic method for writing log message in
  276. given style.
  277. Emits contentChanged signal.
  278. :param line: text line
  279. :param style: text style (see GStc)
  280. :param stdout: write to stdout or stderr
  281. :param notification: form of notification
  282. """
  283. self.cmdOutput.SetStyle()
  284. # documenting old behavior/implementation:
  285. # switch notebook if required
  286. # now, let user to bind to the old event
  287. if not style:
  288. style = self.cmdOutput.StyleDefault
  289. # p1 = self.cmdOutput.GetCurrentPos()
  290. p1 = self.cmdOutput.GetEndStyled()
  291. # self.cmdOutput.GotoPos(p1)
  292. self.cmdOutput.DocumentEnd()
  293. for line in text.splitlines():
  294. # fill space
  295. if len(line) < self.lineWidth:
  296. diff = self.lineWidth - len(line)
  297. line += diff * ' '
  298. self.cmdOutput.AddTextWrapped(line, wrap=wrap) # adds '\n'
  299. p2 = self.cmdOutput.GetCurrentPos()
  300. # between wxWidgets 3.0 and 3.1 they dropped mask param
  301. try:
  302. self.cmdOutput.StartStyling(p1)
  303. except TypeError:
  304. self.cmdOutput.StartStyling(p1, 0xff)
  305. self.cmdOutput.SetStyling(p2 - p1, style)
  306. self.cmdOutput.EnsureCaretVisible()
  307. self.contentChanged.emit(notification=notification)
  308. def WriteCmdLog(self, text, pid=None,
  309. notification=Notification.MAKE_VISIBLE):
  310. """Write message in selected style
  311. :param text: message to be printed
  312. :param pid: process pid or None
  313. :param switchPage: True to switch page
  314. """
  315. if pid:
  316. text = '(' + str(pid) + ') ' + text
  317. self.WriteLog(
  318. text,
  319. style=self.cmdOutput.StyleCommand,
  320. notification=notification)
  321. def WriteWarning(self, text):
  322. """Write message in warning style"""
  323. self.WriteLog(text, style=self.cmdOutput.StyleWarning,
  324. notification=Notification.MAKE_VISIBLE)
  325. def WriteError(self, text):
  326. """Write message in error style"""
  327. self.WriteLog(text, style=self.cmdOutput.StyleError,
  328. notification=Notification.MAKE_VISIBLE)
  329. def OnOutputClear(self, event):
  330. """Clear content of output window"""
  331. self.cmdOutput.SetReadOnly(False)
  332. self.cmdOutput.ClearAll()
  333. self.cmdOutput.SetReadOnly(True)
  334. self.progressbar.SetValue(0)
  335. def GetProgressBar(self):
  336. """Return progress bar widget"""
  337. return self.progressbar
  338. def OnOutputSave(self, event):
  339. """Save (selected) text from output window to the file"""
  340. text = self.cmdOutput.GetSelectedText()
  341. if not text:
  342. text = self.cmdOutput.GetText()
  343. # add newline if needed
  344. if len(text) > 0 and text[-1] != '\n':
  345. text += '\n'
  346. dlg = wx.FileDialog(
  347. self, message=_("Save file as..."),
  348. defaultFile="grass_cmd_output.txt",
  349. wildcard=_("%(txt)s (*.txt)|*.txt|%(files)s (*)|*") %
  350. {'txt': _("Text files"),
  351. 'files': _("Files")},
  352. style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT)
  353. # Show the dialog and retrieve the user response. If it is the OK response,
  354. # process the data.
  355. if dlg.ShowModal() == wx.ID_OK:
  356. path = dlg.GetPath()
  357. try:
  358. output = open(path, "w")
  359. output.write(text)
  360. except IOError as e:
  361. GError(
  362. _("Unable to write file '%(path)s'.\n\nDetails: %(error)s") % {
  363. 'path': path,
  364. 'error': e})
  365. finally:
  366. output.close()
  367. message = _("Command output saved into '%s'") % path
  368. self.showNotification.emit(message=message)
  369. dlg.Destroy()
  370. def SetCopyingOfSelectedText(self, copy):
  371. """Enable or disable copying of selected text in to clipboard.
  372. Effects prompt and output.
  373. :param bool copy: True for enable, False for disable
  374. """
  375. if copy:
  376. self.cmdPrompt.Bind(
  377. stc.EVT_STC_PAINTED,
  378. self.cmdPrompt.OnTextSelectionChanged)
  379. self.cmdOutput.Bind(
  380. stc.EVT_STC_PAINTED,
  381. self.cmdOutput.OnTextSelectionChanged)
  382. else:
  383. self.cmdPrompt.Unbind(stc.EVT_STC_PAINTED)
  384. self.cmdOutput.Unbind(stc.EVT_STC_PAINTED)
  385. def OnCmdOutput(self, event):
  386. """Prints command output.
  387. Emits contentChanged signal.
  388. """
  389. message = event.text
  390. type = event.type
  391. self.cmdOutput.AddStyledMessage(message, type)
  392. if event.type in ('warning', 'error'):
  393. self.contentChanged.emit(notification=Notification.MAKE_VISIBLE)
  394. else:
  395. self.contentChanged.emit(notification=Notification.HIGHLIGHT)
  396. def OnCmdProgress(self, event):
  397. """Update progress message info"""
  398. self.progressbar.SetValue(event.value)
  399. event.Skip()
  400. def CmdProtocolSave(self):
  401. """Save list of manually entered commands into a text log file"""
  402. if self.cmdFileProtocol is None:
  403. return # it should not happen
  404. try:
  405. with open(self.cmdFileProtocol, "a") as output:
  406. cmds = self.cmdPrompt.GetCommands()
  407. output.write('\n'.join(cmds))
  408. if len(cmds) > 0:
  409. output.write('\n')
  410. except IOError as e:
  411. GError(_("Unable to write file '{filePath}'.\n\nDetails: {error}").format(
  412. filePath=self.cmdFileProtocol, error=e))
  413. self.showNotification.emit(
  414. message=_("Command log saved to '{}'".format(self.cmdFileProtocol))
  415. )
  416. self.cmdFileProtocol = None
  417. def OnCmdProtocol(self, event=None):
  418. """Save commands into file"""
  419. if not event.IsChecked():
  420. # stop capturing commands, save list of commands to the
  421. # protocol file
  422. self.CmdProtocolSave()
  423. else:
  424. # start capturing commands
  425. self.cmdPrompt.ClearCommands()
  426. # ask for the file
  427. dlg = wx.FileDialog(
  428. self, message=_("Save file as..."),
  429. defaultFile="grass_cmd_log.txt",
  430. wildcard=_("%(txt)s (*.txt)|*.txt|%(files)s (*)|*") %
  431. {'txt': _("Text files"),
  432. 'files': _("Files")},
  433. style=wx.FD_SAVE)
  434. if dlg.ShowModal() == wx.ID_OK:
  435. self.cmdFileProtocol = dlg.GetPath()
  436. else:
  437. wx.CallAfter(self.btnCmdProtocol.SetValue, False)
  438. dlg.Destroy()
  439. event.Skip()
  440. def OnCmdRun(self, event):
  441. """Run command"""
  442. self.outputSizer.Show(self.panelProgress)
  443. self.outputSizer.Layout()
  444. event.Skip()
  445. def OnCmdDone(self, event):
  446. """Command done (or aborted)
  447. """
  448. self.progressbar.SetValue(0) # reset progress bar on '0%'
  449. wx.CallLater(100, self._hideProgress)
  450. event.Skip()
  451. def _hideProgress(self):
  452. self.outputSizer.Hide(self.panelProgress)
  453. self.outputSizer.Layout()
  454. def ResetFocus(self):
  455. """Reset focus"""
  456. self.cmdPrompt.SetFocus()
  457. def GetPrompt(self):
  458. """Get prompt"""
  459. return self.cmdPrompt
  460. class GStc(stc.StyledTextCtrl):
  461. """Styled text control for GRASS stdout and stderr.
  462. Based on FrameOutErr.py
  463. Name: FrameOutErr.py
  464. Purpose: Redirecting stdout / stderr
  465. Author: Jean-Michel Fauth, Switzerland
  466. Copyright: (c) 2005-2007 Jean-Michel Fauth
  467. Licence: GPL
  468. """
  469. def __init__(self, parent, id, margin=False, wrap=None):
  470. stc.StyledTextCtrl.__init__(self, parent, id)
  471. self.parent = parent
  472. self.SetUndoCollection(True)
  473. self.SetReadOnly(True)
  474. # remember position of line beginning (used for '\r')
  475. self.linePos = -1
  476. #
  477. # styles
  478. #
  479. self.SetStyle()
  480. #
  481. # line margins
  482. #
  483. # TODO print number only from cmdlog
  484. self.SetMarginWidth(1, 0)
  485. self.SetMarginWidth(2, 0)
  486. if margin:
  487. self.SetMarginType(0, stc.STC_MARGIN_NUMBER)
  488. self.SetMarginWidth(0, 30)
  489. else:
  490. self.SetMarginWidth(0, 0)
  491. #
  492. # miscellaneous
  493. #
  494. self.SetViewWhiteSpace(False)
  495. self.SetTabWidth(4)
  496. self.SetUseTabs(False)
  497. self.UsePopUp(True)
  498. self.SetSelBackground(True,
  499. wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHT))
  500. self.SetUseHorizontalScrollBar(True)
  501. #
  502. # bindings
  503. #
  504. self.Bind(wx.EVT_WINDOW_DESTROY, self.OnDestroy)
  505. def OnTextSelectionChanged(self, event):
  506. """Copy selected text to clipboard and skip event.
  507. The same function is in TextCtrlAutoComplete class (prompt.py).
  508. """
  509. wx.CallAfter(self.Copy)
  510. event.Skip()
  511. def SetStyle(self):
  512. """Set styles for styled text output windows with type face
  513. and point size selected by user (Courier New 10 is default)"""
  514. typeface = UserSettings.Get(
  515. group='appearance',
  516. key='outputfont',
  517. subkey='type')
  518. if typeface == "":
  519. typeface = "Courier New"
  520. typesize = UserSettings.Get(
  521. group='appearance',
  522. key='outputfont',
  523. subkey='size')
  524. if typesize is None or int(typesize) <= 0:
  525. typesize = 10
  526. typesize = float(typesize)
  527. fontInfo = wx.FontInfo(typesize)
  528. fontInfo.FaceName(typeface)
  529. fontInfo.Family(wx.FONTFAMILY_TELETYPE)
  530. defaultFont = wx.Font(fontInfo)
  531. self.StyleClearAll()
  532. isDarkMode = False
  533. if wxPythonPhoenix and CheckWxVersion([4, 1, 0]):
  534. isDarkMode = wx.SystemSettings.GetAppearance().IsDark()
  535. defaultBackgroundColour = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW)
  536. defaultTextColour = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOWTEXT)
  537. self.StyleDefault = 0
  538. self.StyleSetFont(stc.STC_STYLE_DEFAULT, defaultFont)
  539. self.StyleSetBackground(stc.STC_STYLE_DEFAULT, defaultBackgroundColour)
  540. self.StyleSetForeground(stc.STC_STYLE_DEFAULT, defaultTextColour)
  541. self.StyleCommand = 1
  542. self.StyleSetBackground(self.StyleCommand, wx.Colour(154, 154, 154, 255))
  543. self.StyleSetForeground(self.StyleCommand, defaultTextColour)
  544. self.StyleOutput = 2
  545. self.StyleSetBackground(self.StyleOutput, defaultBackgroundColour)
  546. self.StyleSetForeground(self.StyleOutput, defaultTextColour)
  547. # fatal error
  548. self.StyleError = 3
  549. errorColour = wx.Colour(127, 0, 0)
  550. if isDarkMode:
  551. errorColour = wx.Colour(230, 0, 0)
  552. self.StyleSetBackground(self.StyleError, defaultBackgroundColour)
  553. self.StyleSetForeground(self.StyleError, errorColour)
  554. # warning
  555. self.StyleWarning = 4
  556. warningColour = wx.Colour(0, 0, 255)
  557. if isDarkMode:
  558. warningColour = wx.Colour(0, 102, 255)
  559. self.StyleSetBackground(self.StyleWarning, defaultBackgroundColour)
  560. self.StyleSetForeground(self.StyleWarning, warningColour)
  561. # message
  562. self.StyleMessage = 5
  563. self.StyleSetBackground(self.StyleMessage, defaultBackgroundColour)
  564. self.StyleSetForeground(self.StyleMessage, defaultTextColour)
  565. # unknown
  566. self.StyleUnknown = 6
  567. self.StyleSetBackground(self.StyleUnknown, defaultBackgroundColour)
  568. self.StyleSetForeground(self.StyleUnknown, defaultTextColour)
  569. def OnDestroy(self, evt):
  570. """The clipboard contents can be preserved after
  571. the app has exited"""
  572. wx.TheClipboard.Flush()
  573. evt.Skip()
  574. def AddTextWrapped(self, txt, wrap=None):
  575. """Add string to text area.
  576. String is wrapped and linesep is also added to the end
  577. of the string"""
  578. # allow writing to output window
  579. self.SetReadOnly(False)
  580. if wrap:
  581. txt = textwrap.fill(txt, wrap) + '\n'
  582. else:
  583. if txt[-1] != '\n':
  584. txt += '\n'
  585. if '\r' in txt:
  586. self.linePos = -1
  587. for seg in txt.split('\r'):
  588. if self.linePos > -1:
  589. self.SetCurrentPos(self.linePos)
  590. self.ReplaceSelection(seg)
  591. else:
  592. self.linePos = self.GetCurrentPos()
  593. self.AddText(seg)
  594. else:
  595. self.linePos = self.GetCurrentPos()
  596. try:
  597. self.AddText(txt)
  598. except UnicodeDecodeError:
  599. # TODO: this might be dead code for Py3, txt is already unicode?
  600. enc = UserSettings.Get(
  601. group='atm', key='encoding', subkey='value')
  602. if enc:
  603. txt = unicode(txt, enc, errors='replace')
  604. elif 'GRASS_DB_ENCODING' in os.environ:
  605. txt = unicode(
  606. txt, os.environ['GRASS_DB_ENCODING'],
  607. errors='replace')
  608. else:
  609. txt = EncodeString(txt)
  610. self.AddText(txt)
  611. # reset output window to read only
  612. self.SetReadOnly(True)
  613. def AddStyledMessage(self, message, style=None):
  614. """Add message to text area.
  615. Handles messages with progress percentages.
  616. :param message: message to be added
  617. :param style: style of message, allowed values: 'message',
  618. 'warning', 'error' or None
  619. """
  620. # message prefix
  621. if style == 'warning':
  622. message = 'WARNING: ' + message
  623. elif style == 'error':
  624. message = 'ERROR: ' + message
  625. p1 = self.GetEndStyled()
  626. self.GotoPos(p1)
  627. # is this still needed?
  628. if '\b' in message:
  629. if self.linePos < 0:
  630. self.linePos = p1
  631. last_c = ''
  632. for c in message:
  633. if c == '\b':
  634. self.linePos -= 1
  635. else:
  636. if c == '\r':
  637. pos = self.GetCurLine()[1]
  638. # self.SetCurrentPos(pos)
  639. else:
  640. self.SetCurrentPos(self.linePos)
  641. self.ReplaceSelection(c)
  642. self.linePos = self.GetCurrentPos()
  643. if c != ' ':
  644. last_c = c
  645. if last_c not in ('0123456789'):
  646. self.AddTextWrapped('\n', wrap=None)
  647. self.linePos = -1
  648. else:
  649. self.linePos = -1 # don't force position
  650. if '\n' not in message:
  651. self.AddTextWrapped(message, wrap=60)
  652. else:
  653. self.AddTextWrapped(message, wrap=None)
  654. p2 = self.GetCurrentPos()
  655. if p2 >= p1:
  656. try:
  657. self.StartStyling(p1)
  658. except TypeError:
  659. self.StartStyling(p1, 0xff)
  660. if style == 'error':
  661. self.SetStyling(p2 - p1, self.StyleError)
  662. elif style == 'warning':
  663. self.SetStyling(p2 - p1, self.StyleWarning)
  664. elif style == 'message':
  665. self.SetStyling(p2 - p1, self.StyleMessage)
  666. else: # unknown
  667. self.SetStyling(p2 - p1, self.StyleUnknown)
  668. self.EnsureCaretVisible()
  669. class GConsoleFrame(wx.Frame):
  670. """Standalone GConsole for testing only"""
  671. def __init__(self, parent, id=wx.ID_ANY, title="GConsole Test Frame",
  672. style=wx.DEFAULT_FRAME_STYLE | wx.TAB_TRAVERSAL, **kwargs):
  673. wx.Frame.__init__(self, parent=parent, id=id, title=title, style=style)
  674. panel = wx.Panel(self, id=wx.ID_ANY)
  675. from lmgr.menudata import LayerManagerMenuData
  676. menuTreeBuilder = LayerManagerMenuData()
  677. self.gconsole = GConsole(guiparent=self)
  678. self.goutput = GConsoleWindow(parent=panel, gconsole=self.gconsole,
  679. menuModel=menuTreeBuilder.GetModel(),
  680. gcstyle=GC_SEARCH | GC_PROMPT)
  681. mainSizer = wx.BoxSizer(wx.VERTICAL)
  682. mainSizer.Add(
  683. self.goutput,
  684. proportion=1,
  685. flag=wx.EXPAND,
  686. border=0)
  687. panel.SetSizer(mainSizer)
  688. mainSizer.Fit(panel)
  689. self.SetMinSize((550, 500))
  690. def testGConsole():
  691. app = wx.App()
  692. frame = GConsoleFrame(parent=None)
  693. frame.Show()
  694. app.MainLoop()
  695. if __name__ == '__main__':
  696. testGConsole()