class Interpreter(PythonEditor): def setup_margin0(self): # Disable 1st margin self.setMarginWidth(0, 0) def show_line_numbers(self): pass def setup_custom(self): self.editor = self self._fontsize = 0 self.wrap() self.history = [] self._outputq = [] self.historyp = -1 self.reading = False self.pt = None self.save_stdout = sys.stdout self.save_stdin = sys.stdin self.save_stderr = sys.stderr sys.stdout = self sys.stderr = self sys.stdin = self self.needmore = False self.cmdthread = None QtCore.QTimer.singleShot(10, self.writeoutputq) self.write('>>> ') def wrap(self): self.setWrapMode(QsciScintilla.SC_WRAP_CHAR) def flush(self): # to suppress AttributeError ... # Not sure why that is happening and why it is not crashing, but... pass def clear(self): self.history = [] self._outputq = [] super().clear() self.write('>>> ') def addcmd(self, cmd, force=False): '''Clear the current line and write the given command to the interpreter''' if not force and self.cmdthread is not None: # Don't bother writing cmd if there is already # a thread in progress... # force=True forces write even if cmd in progress. return self.clearline() self.write(cmd) if cmd[-1] == '\n': self.write('>>> ') self.history.append(cmd.rstrip()) def readline(self): self.reading = True while self.reading: time.sleep(0.1) pt2 = self.pt self.pt = None lenbefore = len(pt2) pt = self.text() r = pt[lenbefore:] r = r.rstrip('\n') if not r: r = '\n' return r def write(self, text): '''cannot write directly to the console... instead, append this text to the output queue for later use. ''' if text: self._outputq.append(text) if len(self._outputq) > 10: time.sleep(.1) if Pen.ControlC == 1: Pen.ControlC += 1 raise KeyboardInterrupt self.save_stdout.write(text) def writeoutputq(self): '''process the text output queue. Must be done from the main thread. ''' while self._outputq: text = self._outputq.pop(0) line, col = self.getCursorPosition() self.insert(text) self.setCursorPosition(line, col + len(text)) QtCore.QTimer.singleShot(100, self.scrolldown) QtCore.QTimer.singleShot(10, self.writeoutputq) def checkprompt(self): line, _col = self.getCursorPosition() txt = self.text(line) if not txt: self.write('>>> ') def cleanup_ControlC(self): Pen.ControlC = False self.cmdthread = None to_remove = [] for pen in self.main_window.user_pens: if not hasattr(pen, 'drawable') or pen.drawable is None: to_remove.append(pen) for pen in to_remove: self.main_window.defunct_pens.append(pen) self.main_window.user_pens.remove(pen) def testthreaddone(self): self.cleanup_ControlC() QtCore.QTimer.singleShot(100, self.checkprompt) def threaddone(self): self.cleanup_ControlC() if not self.needmore: QtCore.QTimer.singleShot(100, self.checkprompt) else: self.write('... ') self.write(' ' * self._indent_level) self._indent_level = 0 def go(self): 'react as if the ENTER key has been pressed on the keyboard' ev = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, QtCore.Qt.Key_Enter, QtCore.Qt.NoModifier, '\n') self.keyPressEvent(ev) def spin(self, n, delay=0.01): '''call processEvents n times. If n is 0, continue until cmdthread is None ''' if n: for _ in range(n): time.sleep(delay) QtGui.QApplication.processEvents(QtCore.QEventLoop.AllEvents) else: self.spin(1) cs = 500 while self.cmdthread is not None: if not cs: LOG.info('spin fail') self.ctrl_c_thread_running() return False cs -= 1 time.sleep(delay) QtGui.QApplication.processEvents(QtCore.QEventLoop.AllEvents) return True def indentation(self, linen): i = 0 txt = self.text(linen)[4:] if txt.isspace(): return len(txt) for i, c in enumerate(txt): if c != ' ': break return i def keyPressEvent(self, ev): k = ev.key() mdf = ev.modifiers() if self.reading and self.pt is None: self.pt = self.text() Tab = QtCore.Qt.Key_Tab Backtab = QtCore.Qt.Key_Backtab Backspace = QtCore.Qt.Key_Backspace Left = QtCore.Qt.Key_Left Right = QtCore.Qt.Key_Right Return = QtCore.Qt.Key_Return Enter = QtCore.Qt.Key_Enter Up = QtCore.Qt.Key_Up PageUp = QtCore.Qt.Key_PageUp Down = QtCore.Qt.Key_Down PageDown = QtCore.Qt.Key_PageDown Control = QtCore.Qt.ControlModifier Shift = QtCore.Qt.ShiftModifier U = QtCore.Qt.Key_U C = QtCore.Qt.Key_C V = QtCore.Qt.Key_V X = QtCore.Qt.Key_X A = QtCore.Qt.Key_A Home = QtCore.Qt.Key_Home E = QtCore.Qt.Key_E D = QtCore.Qt.Key_D H = QtCore.Qt.Key_H Z = QtCore.Qt.Key_Z passthru = True scrolldown = True if k in (Return, Enter): if self.reading: self.reading = False self.movetoend() line, col = self.getCursorPosition() txt = self.text(line) txt = txt[4:].rstrip() if txt: if self.history and not self.history[-1]: # last history entry is blank line del self.history[-1] self.history.append(txt) self.historyp = -1 self.append('\n') if self.cmdthread is None: i = self.indentation(line) if txt.endswith(':'): i += 4 self._indent_level = i self.main_window.pen._mark_undo() self.cmdthread = CmdThread(self, txt) self.cmdthread.start() self.watcherthread = WatcherThread(self.cmdthread) self.watcherthread.finished.connect(self.threaddone) self.watcherthread.start() passthru = False else: passthru = True elif k in (Backspace, Tab, Backtab): line, col = self.getCursorPosition() lines = self.lines() i = self.indentation(line) if line < lines - 1: passthru = False scrolldown = True elif col <= 4 and k != Tab: passthru = False self.scroll_left() elif col <= i + 4: passthru = False spaces = col % 4 if not spaces: spaces = 4 if k != Tab: self.setSelection(line, 4, line, 4 + spaces) self.replaceSelectedText('') self.setCursorPosition(line, col - spaces) else: self.insert(' ' * spaces) self.setCursorPosition(line, 4 + i + spaces) elif line == lines - 1: passthru = True else: passthru = False elif mdf & Shift and k == Up: vbar = self.verticalScrollBar() vbar.setValue(vbar.value() - vbar.singleStep()) passthru = False elif mdf & Shift and k == Down: vbar = self.verticalScrollBar() vbar.setValue(vbar.value() + vbar.singleStep()) passthru = False elif mdf & Shift and k == PageUp: vbar = self.verticalScrollBar() vbar.setValue(vbar.value() - vbar.pageStep()) passthru = False elif mdf & Shift and k == PageDown: vbar = self.verticalScrollBar() vbar.setValue(vbar.value() + vbar.pageStep()) passthru = False elif k == Left: line, col = self.getCursorPosition() lines = self.lines() if line < lines - 1: passthru = False scrolldown = True elif col <= 4: passthru = False self.scroll_left() elif k == Right: line, col = self.getCursorPosition() lines = self.lines() if line < lines - 1: passthru = False scrolldown = True elif k in (Up, Down): self.scrolldown() line, col = self.getCursorPosition() txt = self.text(line)[4:].strip() if self.cmdthread is not None: pass elif not self.history: QtGui.QApplication.beep() else: changeline = True addthisline = False lenhist = len(self.history) if k == Up and self.historyp == -1: addthisline = True if k == Up and lenhist == 1: self.historyp -= 1 elif k == Up and self.historyp <= -lenhist: QtGui.QApplication.beep() changeline = False elif k == Up: self.historyp -= 1 elif k == Down and self.historyp >= -1: QtGui.QApplication.beep() changeline = False elif k == Down: self.historyp += 1 if addthisline: self.history.append(txt) if changeline: txt = self.history[self.historyp] endpos = len(self.text(line)) if self.historyp == -1: del self.history[-1] self.setSelection(line, 4, line, endpos) self.replaceSelectedText(txt) passthru = False elif mdf & Control and k == U: # erase from pen to beginning of line self.scroll_left() self.erasetostart() elif mdf & Control and k == X: # Cut # No action. Disabled. scrolldown = False passthru = False elif mdf & Control and mdf & Shift and k == X: # Cut self.cut() # No action. Disabled. scrolldown = False passthru = False elif mdf & Control and mdf & Shift and k == C: # Copy self.copy() scrolldown = False passthru = False elif mdf & Control and mdf & Shift and k == V: # Paste self.paste() scrolldown = False passthru = False elif mdf & Control and k == Z: self.main_window.pen._undo() scrolldown = True passthru = False elif mdf & Control and k == C: # send keyboard interrupt LOG.info('Ctrl-C pressed') import threading if hasattr(threading, 'threads'): for pen in threading.threads: threading.threads[pen] = 0 if self.cmdthread is not None and self.cmdthread.isAlive(): self.ctrl_c_thread_running() else: self.ctrl_c_no_thread_running() self.sync_pens_lists() elif (mdf & Control and k == A) or k == Home: self.movetostart() self.scroll_left() passthru = False scrolldown = False elif mdf & Control and k == E: self.movetoend() passthru = False elif mdf & Control and k == D: self.main_window.close() passthru = False elif mdf & Control and mdf & Shift and k == H: # Clear history self.history = [] if scrolldown and ev.text(): self.scrolldown() if passthru: super().keyPressEvent(ev) def update_window_modified(self): pass def ctrl_c_thread_running(self): Pen.ControlC = True Pen._stop_testall = True self.main_window.pen._empty_move_queue(lock=True) for pen in self.main_window.user_pens: pen._sync_items() self.needmore = False self.interpreter.resetbuffer() def ctrl_c_no_thread_running(self): Pen.ControlC = False Pen._stop_testall = True self.cmdthread = None self.main_window.pen._empty_move_queue(lock=True) settings = QtCore.QSettings() quiet = settings.value('console/quietinterrupt', False, bool) self.movetoend() if not quiet: self.write('\nKeyboardInterrupt\n') else: self.write('\n') self.interpreter.resetbuffer() self.write('>>> ') def sync_pens_lists(self): for p in self.main_window.all_pens: if p not in self.main_window.user_pens: p.remove() if self.main_window.pen not in self.main_window.user_pens: self.main_window.user_pens.append(self.main_window.pen) def scrolldown(self): '''force the console to scroll all the way down. If the pen is already in the last line, do not change location of the pen. If not, move the pen to the last position in the line. ''' txt = self.text() nlines = len(txt.split('\n')) lastlinen = nlines - 1 lastline = self.text(lastlinen) line, col = self.getCursorPosition() if line != lastlinen: self.setCursorPosition(lastlinen, len(lastline)) if col < 4: self.setCursorPosition(lastlinen, 4) vbar = self.verticalScrollBar() vbar.setValue(vbar.maximum()) def scroll_left(self): hbar = self.horizontalScrollBar() hbar.setValue(hbar.minimum()) def mouseReleaseEvent(self, ev): super().mouseReleaseEvent(ev) line, col = self.getCursorPosition() txt = self.text(line) if col < 4 and not self.hasSelectedText(): if txt.startswith('>>>') or txt.startswith('...'): self.setCursorPosition(line, 4) def contextMenuEvent(self, ev): '''right-click to pop up the context menu. Adjust displayed shortcut key combo since this is an interactive shell and ctrl-c is needed for stopping running code. Connection of actual typed shortcuts is in keyPressEvent() ''' menu = self.createStandardContextMenu() actions = menu.actions() undo = actions[0] redo = actions[1] sep0 = actions[2] cut = actions[3] copy = actions[4] paste = actions[5] delete = actions[6] menu.removeAction(undo) menu.removeAction(redo) menu.removeAction(sep0) menu.removeAction(cut) menu.removeAction(copy) copyaction = QtGui.QAction('Copy', menu) copyshortcut = QtGui.QKeySequence(QtCore.Qt.CTRL + QtCore.Qt.SHIFT + QtCore.Qt.Key_C) copyaction.setShortcut(copyshortcut) copyaction.triggered.connect(self.copy) menu.insertAction(paste, copyaction) menu.removeAction(paste) pasteaction = QtGui.QAction('Paste', menu) pasteshortcut = QtGui.QKeySequence(QtCore.Qt.CTRL + QtCore.Qt.SHIFT + QtCore.Qt.Key_V) pasteaction.setShortcut(pasteshortcut) pasteaction.triggered.connect(self.paste) menu.insertAction(delete, pasteaction) menu.removeAction(delete) menu.exec_(ev.globalPos()) def cut(self): pass def insertFromMimeData(self, data): self.scrolldown() super().insertFromMimeData(data) def movetostart(self): '''move the pen to the start of the line (after the prompt)''' line, _col = self.getCursorPosition() self.setCursorPosition(line, 4) def movetoend(self): '''move the pen to the end of the line''' line, _col = self.getCursorPosition() self.setCursorPosition(line, len(self.text(line))) def erasetostart(self): '''erase from the current pen position to the beginning of the line ''' line, col = self.getCursorPosition() if col == 4: # line is already empty return self.setSelection(line, 4, line, col) self.replaceSelectedText('') self.setCursorPosition(line, 4) def clearline(self): self.scrolldown() self.movetoend() self.erasetostart() def settitle(self): pass def zoomout(self): self.setfontsize(self._fontsize - 1) def setfontsize(self, size): self._fontsize = size self.zoomTo(size)
def keyPressEvent(self, ev): k = ev.key() mdf = ev.modifiers() if self.reading and self.pt is None: self.pt = self.text() Tab = QtCore.Qt.Key_Tab Backtab = QtCore.Qt.Key_Backtab Backspace = QtCore.Qt.Key_Backspace Left = QtCore.Qt.Key_Left Right = QtCore.Qt.Key_Right Return = QtCore.Qt.Key_Return Enter = QtCore.Qt.Key_Enter Up = QtCore.Qt.Key_Up PageUp = QtCore.Qt.Key_PageUp Down = QtCore.Qt.Key_Down PageDown = QtCore.Qt.Key_PageDown Control = QtCore.Qt.ControlModifier Shift = QtCore.Qt.ShiftModifier U = QtCore.Qt.Key_U C = QtCore.Qt.Key_C V = QtCore.Qt.Key_V X = QtCore.Qt.Key_X A = QtCore.Qt.Key_A Home = QtCore.Qt.Key_Home E = QtCore.Qt.Key_E D = QtCore.Qt.Key_D H = QtCore.Qt.Key_H Z = QtCore.Qt.Key_Z passthru = True scrolldown = True if k in (Return, Enter): if self.reading: self.reading = False self.movetoend() line, col = self.getCursorPosition() txt = self.text(line) txt = txt[4:].rstrip() if txt: if self.history and not self.history[-1]: # last history entry is blank line del self.history[-1] self.history.append(txt) self.historyp = -1 self.append('\n') if self.cmdthread is None: i = self.indentation(line) if txt.endswith(':'): i += 4 self._indent_level = i self.main_window.pen._mark_undo() self.cmdthread = CmdThread(self, txt) self.cmdthread.start() self.watcherthread = WatcherThread(self.cmdthread) self.watcherthread.finished.connect(self.threaddone) self.watcherthread.start() passthru = False else: passthru = True elif k in (Backspace, Tab, Backtab): line, col = self.getCursorPosition() lines = self.lines() i = self.indentation(line) if line < lines - 1: passthru = False scrolldown = True elif col <= 4 and k != Tab: passthru = False self.scroll_left() elif col <= i + 4: passthru = False spaces = col % 4 if not spaces: spaces = 4 if k != Tab: self.setSelection(line, 4, line, 4 + spaces) self.replaceSelectedText('') self.setCursorPosition(line, col - spaces) else: self.insert(' ' * spaces) self.setCursorPosition(line, 4 + i + spaces) elif line == lines - 1: passthru = True else: passthru = False elif mdf & Shift and k == Up: vbar = self.verticalScrollBar() vbar.setValue(vbar.value() - vbar.singleStep()) passthru = False elif mdf & Shift and k == Down: vbar = self.verticalScrollBar() vbar.setValue(vbar.value() + vbar.singleStep()) passthru = False elif mdf & Shift and k == PageUp: vbar = self.verticalScrollBar() vbar.setValue(vbar.value() - vbar.pageStep()) passthru = False elif mdf & Shift and k == PageDown: vbar = self.verticalScrollBar() vbar.setValue(vbar.value() + vbar.pageStep()) passthru = False elif k == Left: line, col = self.getCursorPosition() lines = self.lines() if line < lines - 1: passthru = False scrolldown = True elif col <= 4: passthru = False self.scroll_left() elif k == Right: line, col = self.getCursorPosition() lines = self.lines() if line < lines - 1: passthru = False scrolldown = True elif k in (Up, Down): self.scrolldown() line, col = self.getCursorPosition() txt = self.text(line)[4:].strip() if self.cmdthread is not None: pass elif not self.history: QtGui.QApplication.beep() else: changeline = True addthisline = False lenhist = len(self.history) if k == Up and self.historyp == -1: addthisline = True if k == Up and lenhist == 1: self.historyp -= 1 elif k == Up and self.historyp <= -lenhist: QtGui.QApplication.beep() changeline = False elif k == Up: self.historyp -= 1 elif k == Down and self.historyp >= -1: QtGui.QApplication.beep() changeline = False elif k == Down: self.historyp += 1 if addthisline: self.history.append(txt) if changeline: txt = self.history[self.historyp] endpos = len(self.text(line)) if self.historyp == -1: del self.history[-1] self.setSelection(line, 4, line, endpos) self.replaceSelectedText(txt) passthru = False elif mdf & Control and k == U: # erase from pen to beginning of line self.scroll_left() self.erasetostart() elif mdf & Control and k == X: # Cut # No action. Disabled. scrolldown = False passthru = False elif mdf & Control and mdf & Shift and k == X: # Cut self.cut() # No action. Disabled. scrolldown = False passthru = False elif mdf & Control and mdf & Shift and k == C: # Copy self.copy() scrolldown = False passthru = False elif mdf & Control and mdf & Shift and k == V: # Paste self.paste() scrolldown = False passthru = False elif mdf & Control and k == Z: self.main_window.pen._undo() scrolldown = True passthru = False elif mdf & Control and k == C: # send keyboard interrupt LOG.info('Ctrl-C pressed') import threading if hasattr(threading, 'threads'): for pen in threading.threads: threading.threads[pen] = 0 if self.cmdthread is not None and self.cmdthread.isAlive(): self.ctrl_c_thread_running() else: self.ctrl_c_no_thread_running() self.sync_pens_lists() elif (mdf & Control and k == A) or k == Home: self.movetostart() self.scroll_left() passthru = False scrolldown = False elif mdf & Control and k == E: self.movetoend() passthru = False elif mdf & Control and k == D: self.main_window.close() passthru = False elif mdf & Control and mdf & Shift and k == H: # Clear history self.history = [] if scrolldown and ev.text(): self.scrolldown() if passthru: super().keyPressEvent(ev)