diff --git a/IPython/frontend/qt/console/console_widget.py b/IPython/frontend/qt/console/console_widget.py index 79ef1d0..907902e 100644 --- a/IPython/frontend/qt/console/console_widget.py +++ b/IPython/frontend/qt/console/console_widget.py @@ -133,12 +133,15 @@ class ConsoleWidget(QtGui.QPlainTextEdit): def __init__(self, parent=None): QtGui.QPlainTextEdit.__init__(self, parent) - # Initialize protected variables. + # Initialize protected variables. Some variables contain useful state + # information for subclasses; they should be considered read-only. self._ansi_processor = QtAnsiCodeProcessor() self._completion_widget = CompletionWidget(self) self._continuation_prompt = '> ' + self._continuation_prompt_html = None self._executing = False self._prompt = '' + self._prompt_html = None self._prompt_pos = 0 self._reading = False self._reading_callback = None @@ -343,14 +346,34 @@ class ConsoleWidget(QtGui.QPlainTextEdit): # 'QPlainTextEdit' interface #-------------------------------------------------------------------------- + def appendHtml(self, html): + """ Reimplemented to not append HTML as a new paragraph, which doesn't + make sense for a console widget. + """ + cursor = self._get_end_cursor() + cursor.insertHtml(html) + + # After appending HTML, the text document "remembers" the current + # formatting, which means that subsequent calls to 'appendPlainText' + # will be formatted similarly, a behavior that we do not want. To + # prevent this, we make sure that the last character has no formatting. + cursor.movePosition(QtGui.QTextCursor.Left, + QtGui.QTextCursor.KeepAnchor) + if cursor.selection().toPlainText().trimmed().isEmpty(): + # If the last character is whitespace, it doesn't matter how it's + # formatted, so just clear the formatting. + cursor.setCharFormat(QtGui.QTextCharFormat()) + else: + # Otherwise, add an unformatted space. + cursor.movePosition(QtGui.QTextCursor.Right) + cursor.insertText(' ', QtGui.QTextCharFormat()) + def appendPlainText(self, text): """ Reimplemented to not append text as a new paragraph, which doesn't make sense for a console widget. Also, if enabled, handle ANSI codes. """ - cursor = self.textCursor() - cursor.movePosition(QtGui.QTextCursor.End) - + cursor = self._get_end_cursor() if self.ansi_codes: format = QtGui.QTextCharFormat() previous_end = 0 @@ -365,18 +388,15 @@ class ConsoleWidget(QtGui.QPlainTextEdit): cursor.insertText(text) def clear(self, keep_input=False): - """ Reimplemented to cancel reading and write a new prompt. If - 'keep_input' is set, restores the old input buffer when the new - prompt is written. + """ Reimplemented to write a new prompt. If 'keep_input' is set, + restores the old input buffer when the new prompt is written. """ QtGui.QPlainTextEdit.clear(self) - input_buffer = '' - if self._reading: - self._reading = False - elif keep_input: + if keep_input: input_buffer = self.input_buffer self._show_prompt() - self.input_buffer = input_buffer + if keep_input: + self.input_buffer = input_buffer def paste(self): """ Reimplemented to ensure that text is pasted in the editing region. @@ -463,9 +483,6 @@ class ConsoleWidget(QtGui.QPlainTextEdit): cursor = self._get_end_cursor() cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor) - - # Use QTextDocumentFragment intermediate object because it strips - # out the Unicode line break characters that Qt insists on inserting. input_buffer = str(cursor.selection().toPlainText()) # Strip out continuation prompts. @@ -496,7 +513,7 @@ class ConsoleWidget(QtGui.QPlainTextEdit): return None cursor = self.textCursor() if cursor.position() >= self._prompt_pos: - text = str(cursor.block().text()) + text = self._get_block_plain_text(cursor.block()) if cursor.blockNumber() == self._get_prompt_cursor().blockNumber(): return text[len(self._prompt):] else: @@ -581,6 +598,27 @@ class ConsoleWidget(QtGui.QPlainTextEdit): # 'ConsoleWidget' protected interface #-------------------------------------------------------------------------- + def _append_html_fetching_plain_text(self, html): + """ Appends 'html', then returns the plain text version of it. + """ + anchor = self._get_end_cursor().position() + self.appendHtml(html) + cursor = self._get_end_cursor() + cursor.setPosition(anchor, QtGui.QTextCursor.KeepAnchor) + return str(cursor.selection().toPlainText()) + + def _append_plain_text_keeping_prompt(self, text): + """ Writes 'text' after the current prompt, then restores the old prompt + with its old input buffer. + """ + input_buffer = self.input_buffer + self.appendPlainText('\n') + self._prompt_finished() + + self.appendPlainText(text) + self._show_prompt() + self.input_buffer = input_buffer + def _control_down(self, modifiers): """ Given a KeyboardModifiers flags object, return whether the Control key is down (on Mac OS, treat the Command key as a synonym for @@ -607,7 +645,16 @@ class ConsoleWidget(QtGui.QPlainTextEdit): self._completion_widget.show_items(cursor, items) else: text = '\n'.join(items) + '\n' - self._write_text_keeping_prompt(text) + self._append_plain_text_keeping_prompt(text) + + def _get_block_plain_text(self, block): + """ Given a QTextBlock, return its unformatted text. + """ + cursor = QtGui.QTextCursor(block) + cursor.movePosition(QtGui.QTextCursor.StartOfBlock) + cursor.movePosition(QtGui.QTextCursor.EndOfBlock, + QtGui.QTextCursor.KeepAnchor) + return str(cursor.selection().toPlainText()) def _get_end_cursor(self): """ Convenience method that returns a cursor for the last character. @@ -728,6 +775,31 @@ class ConsoleWidget(QtGui.QPlainTextEdit): self._reading_callback = lambda: \ callback(self.input_buffer.rstrip('\n')) + def _reset(self): + """ Clears the console and resets internal state variables. + """ + QtGui.QPlainTextEdit.clear(self) + self._executing = self._reading = False + + def _set_continuation_prompt(self, prompt, html=False): + """ Sets the continuation prompt. + + Parameters + ---------- + prompt : str + The prompt to show when more input is needed. + + html : bool, optional (default False) + If set, the prompt will be inserted as formatted HTML. Otherwise, + the prompt will be treated as plain text, though ANSI color codes + will be handled. + """ + if html: + self._continuation_prompt_html = prompt + else: + self._continuation_prompt = prompt + self._continuation_prompt_html = None + def _set_position(self, position): """ Convenience method to set the position of the cursor. """ @@ -740,7 +812,7 @@ class ConsoleWidget(QtGui.QPlainTextEdit): """ self.setTextCursor(self._get_selection_cursor(start, end)) - def _show_prompt(self, prompt=None, newline=True): + def _show_prompt(self, prompt=None, html=False, newline=True): """ Writes a new prompt at the end of the buffer. Parameters @@ -748,10 +820,16 @@ class ConsoleWidget(QtGui.QPlainTextEdit): prompt : str, optional The prompt to show. If not specified, the previous prompt is used. + html : bool, optional (default False) + Only relevant when a prompt is specified. If set, the prompt will + be inserted as formatted HTML. Otherwise, the prompt will be treated + as plain text, though ANSI color codes will be handled. + newline : bool, optional (default True) If set, a new line will be written before showing the prompt if there is not already a newline at the end of the buffer. """ + # Insert a preliminary newline, if necessary. if newline: cursor = self._get_end_cursor() if cursor.position() > 0: @@ -760,9 +838,20 @@ class ConsoleWidget(QtGui.QPlainTextEdit): if str(cursor.selection().toPlainText()) != '\n': self.appendPlainText('\n') - if prompt is not None: - self._prompt = prompt - self.appendPlainText(self._prompt) + # Write the prompt. + if prompt is None: + if self._prompt_html is None: + self.appendPlainText(self._prompt) + else: + self.appendHtml(self._prompt_html) + else: + if html: + self._prompt = self._append_html_fetching_plain_text(prompt) + self._prompt_html = prompt + else: + self.appendPlainText(prompt) + self._prompt = prompt + self._prompt_html = None self._prompt_pos = self._get_end_cursor().position() self._prompt_started() @@ -770,20 +859,13 @@ class ConsoleWidget(QtGui.QPlainTextEdit): def _show_continuation_prompt(self): """ Writes a new continuation prompt at the end of the buffer. """ - self.appendPlainText(self._continuation_prompt) - self._prompt_started() - - def _write_text_keeping_prompt(self, text): - """ Writes 'text' after the current prompt, then restores the old prompt - with its old input buffer. - """ - input_buffer = self.input_buffer - self.appendPlainText('\n') - self._prompt_finished() + if self._continuation_prompt_html is None: + self.appendPlainText(self._continuation_prompt) + else: + self._continuation_prompt = self._append_html_fetching_plain_text( + self._continuation_prompt_html) - self.appendPlainText(text) - self._show_prompt() - self.input_buffer = input_buffer + self._prompt_started() def _in_buffer(self, position): """ Returns whether the given position is inside the editing region. diff --git a/IPython/frontend/qt/console/frontend_widget.py b/IPython/frontend/qt/console/frontend_widget.py index db1ab48..7890f07 100644 --- a/IPython/frontend/qt/console/frontend_widget.py +++ b/IPython/frontend/qt/console/frontend_widget.py @@ -16,12 +16,12 @@ from pygments_highlighter import PygmentsHighlighter class FrontendHighlighter(PygmentsHighlighter): - """ A Python PygmentsHighlighter that can be turned on and off and which - knows about continuation prompts. + """ A PygmentsHighlighter that can be turned on and off and that ignores + prompts. """ def __init__(self, frontend): - PygmentsHighlighter.__init__(self, frontend.document(), PythonLexer()) + super(FrontendHighlighter, self).__init__(frontend.document()) self._current_offset = 0 self._frontend = frontend self.highlighting_on = False @@ -29,17 +29,32 @@ class FrontendHighlighter(PygmentsHighlighter): def highlightBlock(self, qstring): """ Highlight a block of text. Reimplemented to highlight selectively. """ - if self.highlighting_on: - for prompt in (self._frontend._continuation_prompt, - self._frontend._prompt): - if qstring.startsWith(prompt): - qstring.remove(0, len(prompt)) - self._current_offset = len(prompt) - break - PygmentsHighlighter.highlightBlock(self, qstring) + if not self.highlighting_on: + return + + # The input to this function is unicode string that may contain + # paragraph break characters, non-breaking spaces, etc. Here we acquire + # the string as plain text so we can compare it. + current_block = self.currentBlock() + string = self._frontend._get_block_plain_text(current_block) + + # Decide whether to check for the regular or continuation prompt. + if current_block.contains(self._frontend._prompt_pos): + prompt = self._frontend._prompt + else: + prompt = self._frontend._continuation_prompt + + # Don't highlight the part of the string that contains the prompt. + if string.startswith(prompt): + self._current_offset = len(prompt) + qstring.remove(0, len(prompt)) + else: + self._current_offset = 0 + + PygmentsHighlighter.highlightBlock(self, qstring) def setFormat(self, start, count, format): - """ Reimplemented to avoid highlighting continuation prompts. + """ Reimplemented to highlight selectively. """ start += self._current_offset PygmentsHighlighter.setFormat(self, start, count, format) @@ -59,9 +74,6 @@ class FrontendWidget(HistoryConsoleWidget): def __init__(self, parent=None): super(FrontendWidget, self).__init__(parent) - # ConsoleWidget protected variables. - self._continuation_prompt = '... ' - # FrontendWidget protected variables. self._call_tip_widget = CallTipWidget(self) self._completion_lexer = CompletionLexer(PythonLexer()) @@ -70,6 +82,9 @@ class FrontendWidget(HistoryConsoleWidget): self._input_splitter = InputSplitter(input_mode='replace') self._kernel_manager = None + # Configure the ConsoleWidget. + self._set_continuation_prompt('... ') + self.document().contentsChange.connect(self._document_contents_change) #--------------------------------------------------------------------------- @@ -143,17 +158,6 @@ class FrontendWidget(HistoryConsoleWidget): return False #--------------------------------------------------------------------------- - # 'ConsoleWidget' protected interface - #--------------------------------------------------------------------------- - - def _show_prompt(self, prompt=None, newline=True): - """ Reimplemented to set a default prompt. - """ - if prompt is None: - prompt = '>>> ' - super(FrontendWidget, self)._show_prompt(prompt, newline) - - #--------------------------------------------------------------------------- # 'FrontendWidget' interface #--------------------------------------------------------------------------- @@ -283,20 +287,24 @@ class FrontendWidget(HistoryConsoleWidget): self.appendPlainText('Kernel process is either remote or ' 'unspecified. Cannot interrupt.\n') + def _show_interpreter_prompt(self): + """ Shows a prompt for the interpreter. + """ + self._show_prompt('>>> ') + #------ Signal handlers ---------------------------------------------------- def _started_channels(self): """ Called when the kernel manager has started listening. """ - QtGui.QPlainTextEdit.clear(self) - if self._reading: - self._reading = False + self._reset() self.appendPlainText(self._get_banner()) - self._show_prompt() + self._show_interpreter_prompt() def _stopped_channels(self): """ Called when the kernel manager has stopped listening. """ + # FIXME: Print a message here? pass def _document_contents_change(self, position, removed, added): @@ -327,9 +335,7 @@ class FrontendWidget(HistoryConsoleWidget): handler(omsg) def _handle_pyout(self, omsg): - session = omsg['parent_header']['session'] - if session == self.kernel_manager.session.session: - self.appendPlainText(omsg['content']['data'] + '\n') + self.appendPlainText(omsg['content']['data'] + '\n') def _handle_stream(self, omsg): self.appendPlainText(omsg['content']['data']) @@ -351,7 +357,7 @@ class FrontendWidget(HistoryConsoleWidget): text = "ERROR: ABORTED\n" self.appendPlainText(text) self._hidden = True - self._show_prompt() + self._show_interpreter_prompt() self.executed.emit(rep) def _handle_complete_reply(self, rep): diff --git a/IPython/frontend/qt/console/ipython_widget.py b/IPython/frontend/qt/console/ipython_widget.py index ee08223..1b950b2 100644 --- a/IPython/frontend/qt/console/ipython_widget.py +++ b/IPython/frontend/qt/console/ipython_widget.py @@ -10,6 +10,14 @@ class IPythonWidget(FrontendWidget): """ A FrontendWidget for an IPython kernel. """ + # The default stylesheet for prompts, colors, etc. + default_stylesheet = """ + .in-prompt { color: navy; } + .in-prompt-number { font-weight: bold; } + .out-prompt { color: darkred; } + .out-prompt-number { font-weight: bold; } + """ + #--------------------------------------------------------------------------- # 'QObject' interface #--------------------------------------------------------------------------- @@ -17,7 +25,12 @@ class IPythonWidget(FrontendWidget): def __init__(self, parent=None): super(IPythonWidget, self).__init__(parent) + # Initialize protected variables. self._magic_overrides = {} + self._prompt_count = 0 + + # Set a default stylesheet. + self.set_style_sheet(self.default_stylesheet) #--------------------------------------------------------------------------- # 'ConsoleWidget' abstract interface @@ -38,7 +51,7 @@ class IPythonWidget(FrontendWidget): output = callback(arguments) if output: self.appendPlainText(output) - self._show_prompt() + self._show_interpreter_prompt() else: super(IPythonWidget, self)._execute(source, hidden) @@ -56,10 +69,36 @@ class IPythonWidget(FrontendWidget): #--------------------------------------------------------------------------- def _get_banner(self): - """ Reimplemented to a return IPython's default banner. + """ Reimplemented to return IPython's default banner. """ return default_banner + def _show_interpreter_prompt(self): + """ Reimplemented for IPython-style prompts. + """ + self._prompt_count += 1 + prompt_template = '%s' + prompt_body = '
In [%i]: ' + prompt = (prompt_template % prompt_body) % self._prompt_count + self._show_prompt(prompt, html=True) + + # Update continuation prompt to reflect (possibly) new prompt length. + cont_prompt_chars = '...: ' + space_count = len(self._prompt.lstrip()) - len(cont_prompt_chars) + cont_prompt_body = ' ' * space_count + cont_prompt_chars + self._continuation_prompt_html = prompt_template % cont_prompt_body + + #------ Signal handlers ---------------------------------------------------- + + def _handle_pyout(self, omsg): + """ Reimplemented for IPython-style "display hook". + """ + prompt_template = '%s' + prompt_body = 'Out[%i]: ' + prompt = (prompt_template % prompt_body) % self._prompt_count + self.appendHtml(prompt) + self.appendPlainText(omsg['content']['data'] + '\n') + #--------------------------------------------------------------------------- # 'IPythonWidget' interface #--------------------------------------------------------------------------- @@ -81,6 +120,11 @@ class IPythonWidget(FrontendWidget): except KeyError: pass + def set_style_sheet(self, stylesheet): + """ Sets the style sheet. + """ + self.document().setDefaultStyleSheet(stylesheet) + if __name__ == '__main__': from IPython.frontend.qt.kernelmanager import QtKernelManager diff --git a/IPython/frontend/qt/console/pygments_highlighter.py b/IPython/frontend/qt/console/pygments_highlighter.py index 51197a8..4b45dc9 100644 --- a/IPython/frontend/qt/console/pygments_highlighter.py +++ b/IPython/frontend/qt/console/pygments_highlighter.py @@ -1,7 +1,7 @@ # System library imports. from PyQt4 import QtGui from pygments.lexer import RegexLexer, _TokenType, Text, Error -from pygments.lexers import CLexer, CppLexer, PythonLexer +from pygments.lexers import PythonLexer from pygments.styles.default import DefaultStyle from pygments.token import Comment @@ -133,7 +133,7 @@ class PygmentsHighlighter(QtGui.QSyntaxHighlighter): if token in self._formats: return self._formats[token] result = None - for key, value in self._style.style_for_token(token) .items(): + for key, value in self._style.style_for_token(token).items(): if value: if result is None: result = QtGui.QTextCharFormat() @@ -171,12 +171,11 @@ class PygmentsHighlighter(QtGui.QSyntaxHighlighter): qcolor = self._get_color(color) result = QtGui.QBrush(qcolor) self._brushes[color] = result - return result def _get_color(self, color): qcolor = QtGui.QColor() - qcolor.setRgb(int(color[:2],base=16), + qcolor.setRgb(int(color[:2], base=16), int(color[2:4], base=16), int(color[4:6], base=16)) return qcolor