1# consolewindow.py 2# a python-like qt console 3 4# Copyright (C) 2003 Jeremy S. Sanders 5# Email: Jeremy Sanders <jeremy@jeremysanders.net> 6# 7# This program is free software; you can redistribute it and/or modify 8# it under the terms of the GNU General Public License as published by 9# the Free Software Foundation; either version 2 of the License, or 10# (at your option) any later version. 11# 12# This program is distributed in the hope that it will be useful, 13# but WITHOUT ANY WARRANTY; without even the implied warranty of 14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15# GNU General Public License for more details. 16# 17# You should have received a copy of the GNU General Public License along 18# with this program; if not, write to the Free Software Foundation, Inc., 19# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 20############################################################################## 21 22from __future__ import division 23import codeop 24import traceback 25import sys 26 27from ..compat import cstr 28from .. import qtall as qt 29 30from .. import document 31from .. import utils 32from .. import setting 33 34# TODO - command line completion 35 36def _(text, disambiguation=None, context='ConsoleWindow'): 37 """Translate text.""" 38 return qt.QCoreApplication.translate(context, text, disambiguation) 39 40class _Writer(object): 41 """ Class to behave like an output stream. Pipes input back to 42 the specified function.""" 43 44 def __init__(self, function): 45 """Set the function output is sent to.""" 46 self.function = function 47 48 def write(self, text): 49 """Send text to the output function.""" 50 self.function(text) 51 52 def flush(self): 53 """Does nothing as yet.""" 54 pass 55 56class _Reader(object): 57 """Fake reading input stream object.""" 58 def read(self, *args): 59 raise IOError('Interactive input not supported') 60 def readline(self, *args): 61 raise IOError('Interactive input not supported') 62 63class _CommandEdit(qt.QLineEdit): 64 """ A special class to allow entering of the command line. 65 66 emits sigEnter if the return key is pressed, and returns command 67 The edit control has a history (press up and down keys to access) 68 """ 69 70 sigEnter = qt.pyqtSignal(cstr) 71 72 def __init__(self, *args): 73 qt.QLineEdit.__init__(self, *args) 74 self.history = [] 75 self.history_posn = 0 76 self.entered_text = '' 77 78 self.returnPressed.connect(self.slotReturnPressed) 79 80 self.setToolTip(_("Input a python expression here and press enter")) 81 82 def slotReturnPressed(self): 83 """ Called if the return key is pressed in the edit control.""" 84 85 # retrieve the text 86 command = self.text() 87 self.setText("") 88 89 # keep the command for history 90 self.history.append(command) 91 self.history_posn = len(self.history) 92 self.entered_text = '' 93 94 # tell the console we have a command 95 self.sigEnter.emit(command) 96 97 historykeys = (qt.Qt.Key_Up, qt.Qt.Key_Down) 98 99 def keyPressEvent(self, key): 100 """ Overridden to handle history. """ 101 102 qt.QLineEdit.keyPressEvent(self, key) 103 code = key.key() 104 105 # check whether one of the "history keys" has been pressed 106 if code in _CommandEdit.historykeys: 107 108 # look for the next or previous history item which our current text 109 # is a prefix of 110 if self.isModified(): 111 text = self.text() 112 self.history_posn = len(self.history) 113 else: 114 text = self.entered_text 115 116 if code == qt.Qt.Key_Up: 117 step = -1 118 elif code == qt.Qt.Key_Down: 119 step = 1 120 121 newpos = self.history_posn + step 122 123 while True: 124 if newpos >= len(self.history): 125 break 126 if newpos < 0: 127 return 128 if self.history[newpos].startswith(text): 129 break 130 131 newpos += step 132 133 if newpos >= len(self.history): 134 # go back to whatever the user had typed in 135 self.history_posn = len(self.history) 136 self.setText(self.entered_text) 137 return 138 139 # found a relevant history item 140 self.history_posn = newpos 141 142 # user has modified text since last set 143 if self.isModified(): 144 self.entered_text = text 145 146 # replace the text in the control 147 text = self.history[ self.history_posn ] 148 self.setText(text) 149 150introtext=_(u'''Welcome to <b><font color="purple">Veusz %s</font></b> --- a scientific plotting application.<br> 151Copyright \u00a9 2003-2020 Jeremy Sanders <jeremy@jeremysanders.net> and contributors.<br> 152Veusz comes with ABSOLUTELY NO WARRANTY. Veusz is Free Software, and you are<br> 153welcome to redistribute it under certain conditions. Enter "GPL()" for details.<br> 154This window is a Python command line console and acts as a calculator.<br> 155''') % utils.version() 156 157class ConsoleWindow(qt.QDockWidget): 158 """ A python-like qt console.""" 159 160 def __init__(self, thedocument, *args): 161 qt.QDockWidget.__init__(self, *args) 162 self.setWindowTitle(_("Console - Veusz")) 163 self.setObjectName("veuszconsolewindow") 164 165 # arrange sub-widgets in a vbox 166 self.vbox = qt.QWidget() 167 self.setWidget(self.vbox) 168 vlayout = qt.QVBoxLayout(self.vbox) 169 s = vlayout.contentsMargins().left()//4 170 vlayout.setContentsMargins(s,s,s,s) 171 vlayout.setSpacing(s) 172 173 # start an interpreter instance to the document 174 self.interpreter = document.CommandInterpreter(thedocument) 175 self.document = thedocument 176 177 # streams the output/input goes to/from 178 self.con_stdout = _Writer(self.output_stdout) 179 self.con_stderr = _Writer(self.output_stderr) 180 self.con_stdin = _Reader() 181 182 self.interpreter.setFiles( 183 self.con_stdout, self.con_stderr, self.con_stdin) 184 self.stdoutbuffer = "" 185 self.stderrbuffer = "" 186 187 # (mostly) hidden notification 188 self._hiddennotify = qt.QLabel() 189 vlayout.addWidget(self._hiddennotify) 190 self._hiddennotify.hide() 191 192 # the output from the console goes here 193 self._outputdisplay = qt.QTextEdit() 194 self._outputdisplay.setReadOnly(True) 195 self._outputdisplay.insertHtml( introtext ) 196 vlayout.addWidget(self._outputdisplay) 197 198 self._hbox = qt.QWidget() 199 hlayout = qt.QHBoxLayout(self._hbox) 200 hlayout.setContentsMargins(0,0,0,0) 201 vlayout.addWidget(self._hbox) 202 203 self._prompt = qt.QLabel(">>>") 204 hlayout.addWidget(self._prompt) 205 206 # where commands are typed in 207 self._inputedit = _CommandEdit() 208 hlayout.addWidget(self._inputedit) 209 self._inputedit.setFocus() 210 211 # keep track of multiple line commands 212 self.command_build = '' 213 214 # get called if enter is pressed in the input control 215 self._inputedit.sigEnter.connect(self.slotEnter) 216 # called if document logs something 217 thedocument.sigLog.connect(self.slotDocumentLog) 218 219 def _makeTextFormat(self, cursor, color): 220 fmt = cursor.charFormat() 221 222 if color is not None: 223 brush = qt.QBrush(color) 224 fmt.setForeground(brush) 225 else: 226 # use the default foreground color 227 fmt.clearForeground() 228 229 return fmt 230 231 def appendOutput(self, text, style): 232 """Add text to the tail of the error log, with a specified style""" 233 if style == 'error': 234 color = setting.settingdb.color('error') 235 elif style == 'command': 236 color = setting.settingdb.color('command') 237 else: 238 color = None 239 240 cursor = self._outputdisplay.textCursor() 241 cursor.movePosition(qt.QTextCursor.End) 242 cursor.insertText(text, self._makeTextFormat(cursor, color)) 243 self._outputdisplay.setTextCursor(cursor) 244 self._outputdisplay.ensureCursorVisible() 245 246 def runFunction(self, func): 247 """Execute the function within the console window, trapping 248 exceptions.""" 249 250 # preserve output streams 251 saved = sys.stdout, sys.stderr, sys.stdin 252 sys.stdout, sys.stderr, sys.stdin = ( 253 self.con_stdout, self.con_stderr, self.con_stdin) 254 255 # catch any exceptions, printing problems to stderr 256 with self.document.suspend(): 257 try: 258 func() 259 except: 260 # print out the backtrace to stderr 261 info = sys.exc_info() 262 backtrace = traceback.format_exception(*info) 263 for line in backtrace: 264 sys.stderr.write(line) 265 266 # return output streams 267 sys.stdout, sys.stderr, sys.stdin = saved 268 269 def checkVisible(self): 270 """If this window is hidden, show it, then hide it again in a few 271 seconds.""" 272 if self.isHidden(): 273 self._hiddennotify.setText(_( 274 "This window will shortly disappear. " 275 "You can bring it back by selecting " 276 "View, Windows, Console Window on the menu.")) 277 qt.QTimer.singleShot(5000, self.hideConsole) 278 self.show() 279 self._hiddennotify.show() 280 281 def hideConsole(self): 282 """Hide window and notification widget.""" 283 self._hiddennotify.hide() 284 self.hide() 285 286 def output_stdout(self, text): 287 """ Write text in stdout font to the log.""" 288 self.checkVisible() 289 self.appendOutput(text, 'normal') 290 291 def output_stderr(self, text): 292 """ Write text in stderr font to the log.""" 293 self.checkVisible() 294 self.appendOutput(text, 'error') 295 296 def insertTextInOutput(self, text): 297 """ Inserts the text into the log.""" 298 self.appendOutput(text, 'normal') 299 300 def slotEnter(self, command): 301 """ Called if the return key is pressed in the edit control.""" 302 303 newc = self.command_build + '\n' + command 304 305 # check whether command can be compiled 306 # c set to None if incomplete 307 try: 308 comp = codeop.compile_command(newc) 309 except Exception: 310 # we want errors to be caught by self.interpreter.run below 311 comp = 1 312 313 # which prompt? 314 prompt = '>>>' 315 if self.command_build != '': 316 prompt = '...' 317 318 # output the command in the log pane 319 self.appendOutput('%s %s\n' % (prompt, command), 'command') 320 321 # are we ready to run this? 322 if comp is None or ( 323 len(command) != 0 and 324 len(self.command_build) != 0 and 325 (command[0] == ' ' or command[0] == '\t')): 326 # build up the expression 327 self.command_build = newc 328 # modify the prompt 329 self._prompt.setText('...') 330 else: 331 # actually execute the command 332 self.interpreter.run(newc) 333 self.command_build = '' 334 # modify the prompt 335 self._prompt.setText('>>>') 336 337 def slotDocumentLog(self, text): 338 """Output information if the document logs something.""" 339 self.output_stderr(text + '\n') 340