goutput.py 26 KB

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