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