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