##// END OF EJS Templates
* IPythonWidget now has IPython-style prompts that are futher stylabla via CSS...
epatters -
Show More
@@ -133,12 +133,15 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
133 133 def __init__(self, parent=None):
134 134 QtGui.QPlainTextEdit.__init__(self, parent)
135 135
136 # Initialize protected variables.
136 # Initialize protected variables. Some variables contain useful state
137 # information for subclasses; they should be considered read-only.
137 138 self._ansi_processor = QtAnsiCodeProcessor()
138 139 self._completion_widget = CompletionWidget(self)
139 140 self._continuation_prompt = '> '
141 self._continuation_prompt_html = None
140 142 self._executing = False
141 143 self._prompt = ''
144 self._prompt_html = None
142 145 self._prompt_pos = 0
143 146 self._reading = False
144 147 self._reading_callback = None
@@ -343,14 +346,34 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
343 346 # 'QPlainTextEdit' interface
344 347 #--------------------------------------------------------------------------
345 348
349 def appendHtml(self, html):
350 """ Reimplemented to not append HTML as a new paragraph, which doesn't
351 make sense for a console widget.
352 """
353 cursor = self._get_end_cursor()
354 cursor.insertHtml(html)
355
356 # After appending HTML, the text document "remembers" the current
357 # formatting, which means that subsequent calls to 'appendPlainText'
358 # will be formatted similarly, a behavior that we do not want. To
359 # prevent this, we make sure that the last character has no formatting.
360 cursor.movePosition(QtGui.QTextCursor.Left,
361 QtGui.QTextCursor.KeepAnchor)
362 if cursor.selection().toPlainText().trimmed().isEmpty():
363 # If the last character is whitespace, it doesn't matter how it's
364 # formatted, so just clear the formatting.
365 cursor.setCharFormat(QtGui.QTextCharFormat())
366 else:
367 # Otherwise, add an unformatted space.
368 cursor.movePosition(QtGui.QTextCursor.Right)
369 cursor.insertText(' ', QtGui.QTextCharFormat())
370
346 371 def appendPlainText(self, text):
347 372 """ Reimplemented to not append text as a new paragraph, which doesn't
348 373 make sense for a console widget. Also, if enabled, handle ANSI
349 374 codes.
350 375 """
351 cursor = self.textCursor()
352 cursor.movePosition(QtGui.QTextCursor.End)
353
376 cursor = self._get_end_cursor()
354 377 if self.ansi_codes:
355 378 format = QtGui.QTextCharFormat()
356 379 previous_end = 0
@@ -365,18 +388,15 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
365 388 cursor.insertText(text)
366 389
367 390 def clear(self, keep_input=False):
368 """ Reimplemented to cancel reading and write a new prompt. If
369 'keep_input' is set, restores the old input buffer when the new
370 prompt is written.
391 """ Reimplemented to write a new prompt. If 'keep_input' is set,
392 restores the old input buffer when the new prompt is written.
371 393 """
372 394 QtGui.QPlainTextEdit.clear(self)
373 input_buffer = ''
374 if self._reading:
375 self._reading = False
376 elif keep_input:
395 if keep_input:
377 396 input_buffer = self.input_buffer
378 397 self._show_prompt()
379 self.input_buffer = input_buffer
398 if keep_input:
399 self.input_buffer = input_buffer
380 400
381 401 def paste(self):
382 402 """ Reimplemented to ensure that text is pasted in the editing region.
@@ -463,9 +483,6 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
463 483
464 484 cursor = self._get_end_cursor()
465 485 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
466
467 # Use QTextDocumentFragment intermediate object because it strips
468 # out the Unicode line break characters that Qt insists on inserting.
469 486 input_buffer = str(cursor.selection().toPlainText())
470 487
471 488 # Strip out continuation prompts.
@@ -496,7 +513,7 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
496 513 return None
497 514 cursor = self.textCursor()
498 515 if cursor.position() >= self._prompt_pos:
499 text = str(cursor.block().text())
516 text = self._get_block_plain_text(cursor.block())
500 517 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
501 518 return text[len(self._prompt):]
502 519 else:
@@ -581,6 +598,27 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
581 598 # 'ConsoleWidget' protected interface
582 599 #--------------------------------------------------------------------------
583 600
601 def _append_html_fetching_plain_text(self, html):
602 """ Appends 'html', then returns the plain text version of it.
603 """
604 anchor = self._get_end_cursor().position()
605 self.appendHtml(html)
606 cursor = self._get_end_cursor()
607 cursor.setPosition(anchor, QtGui.QTextCursor.KeepAnchor)
608 return str(cursor.selection().toPlainText())
609
610 def _append_plain_text_keeping_prompt(self, text):
611 """ Writes 'text' after the current prompt, then restores the old prompt
612 with its old input buffer.
613 """
614 input_buffer = self.input_buffer
615 self.appendPlainText('\n')
616 self._prompt_finished()
617
618 self.appendPlainText(text)
619 self._show_prompt()
620 self.input_buffer = input_buffer
621
584 622 def _control_down(self, modifiers):
585 623 """ Given a KeyboardModifiers flags object, return whether the Control
586 624 key is down (on Mac OS, treat the Command key as a synonym for
@@ -607,7 +645,16 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
607 645 self._completion_widget.show_items(cursor, items)
608 646 else:
609 647 text = '\n'.join(items) + '\n'
610 self._write_text_keeping_prompt(text)
648 self._append_plain_text_keeping_prompt(text)
649
650 def _get_block_plain_text(self, block):
651 """ Given a QTextBlock, return its unformatted text.
652 """
653 cursor = QtGui.QTextCursor(block)
654 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
655 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
656 QtGui.QTextCursor.KeepAnchor)
657 return str(cursor.selection().toPlainText())
611 658
612 659 def _get_end_cursor(self):
613 660 """ Convenience method that returns a cursor for the last character.
@@ -728,6 +775,31 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
728 775 self._reading_callback = lambda: \
729 776 callback(self.input_buffer.rstrip('\n'))
730 777
778 def _reset(self):
779 """ Clears the console and resets internal state variables.
780 """
781 QtGui.QPlainTextEdit.clear(self)
782 self._executing = self._reading = False
783
784 def _set_continuation_prompt(self, prompt, html=False):
785 """ Sets the continuation prompt.
786
787 Parameters
788 ----------
789 prompt : str
790 The prompt to show when more input is needed.
791
792 html : bool, optional (default False)
793 If set, the prompt will be inserted as formatted HTML. Otherwise,
794 the prompt will be treated as plain text, though ANSI color codes
795 will be handled.
796 """
797 if html:
798 self._continuation_prompt_html = prompt
799 else:
800 self._continuation_prompt = prompt
801 self._continuation_prompt_html = None
802
731 803 def _set_position(self, position):
732 804 """ Convenience method to set the position of the cursor.
733 805 """
@@ -740,7 +812,7 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
740 812 """
741 813 self.setTextCursor(self._get_selection_cursor(start, end))
742 814
743 def _show_prompt(self, prompt=None, newline=True):
815 def _show_prompt(self, prompt=None, html=False, newline=True):
744 816 """ Writes a new prompt at the end of the buffer.
745 817
746 818 Parameters
@@ -748,10 +820,16 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
748 820 prompt : str, optional
749 821 The prompt to show. If not specified, the previous prompt is used.
750 822
823 html : bool, optional (default False)
824 Only relevant when a prompt is specified. If set, the prompt will
825 be inserted as formatted HTML. Otherwise, the prompt will be treated
826 as plain text, though ANSI color codes will be handled.
827
751 828 newline : bool, optional (default True)
752 829 If set, a new line will be written before showing the prompt if
753 830 there is not already a newline at the end of the buffer.
754 831 """
832 # Insert a preliminary newline, if necessary.
755 833 if newline:
756 834 cursor = self._get_end_cursor()
757 835 if cursor.position() > 0:
@@ -760,9 +838,20 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
760 838 if str(cursor.selection().toPlainText()) != '\n':
761 839 self.appendPlainText('\n')
762 840
763 if prompt is not None:
764 self._prompt = prompt
765 self.appendPlainText(self._prompt)
841 # Write the prompt.
842 if prompt is None:
843 if self._prompt_html is None:
844 self.appendPlainText(self._prompt)
845 else:
846 self.appendHtml(self._prompt_html)
847 else:
848 if html:
849 self._prompt = self._append_html_fetching_plain_text(prompt)
850 self._prompt_html = prompt
851 else:
852 self.appendPlainText(prompt)
853 self._prompt = prompt
854 self._prompt_html = None
766 855
767 856 self._prompt_pos = self._get_end_cursor().position()
768 857 self._prompt_started()
@@ -770,20 +859,13 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
770 859 def _show_continuation_prompt(self):
771 860 """ Writes a new continuation prompt at the end of the buffer.
772 861 """
773 self.appendPlainText(self._continuation_prompt)
774 self._prompt_started()
775
776 def _write_text_keeping_prompt(self, text):
777 """ Writes 'text' after the current prompt, then restores the old prompt
778 with its old input buffer.
779 """
780 input_buffer = self.input_buffer
781 self.appendPlainText('\n')
782 self._prompt_finished()
862 if self._continuation_prompt_html is None:
863 self.appendPlainText(self._continuation_prompt)
864 else:
865 self._continuation_prompt = self._append_html_fetching_plain_text(
866 self._continuation_prompt_html)
783 867
784 self.appendPlainText(text)
785 self._show_prompt()
786 self.input_buffer = input_buffer
868 self._prompt_started()
787 869
788 870 def _in_buffer(self, position):
789 871 """ Returns whether the given position is inside the editing region.
@@ -16,12 +16,12 b' from pygments_highlighter import PygmentsHighlighter'
16 16
17 17
18 18 class FrontendHighlighter(PygmentsHighlighter):
19 """ A Python PygmentsHighlighter that can be turned on and off and which
20 knows about continuation prompts.
19 """ A PygmentsHighlighter that can be turned on and off and that ignores
20 prompts.
21 21 """
22 22
23 23 def __init__(self, frontend):
24 PygmentsHighlighter.__init__(self, frontend.document(), PythonLexer())
24 super(FrontendHighlighter, self).__init__(frontend.document())
25 25 self._current_offset = 0
26 26 self._frontend = frontend
27 27 self.highlighting_on = False
@@ -29,17 +29,32 b' class FrontendHighlighter(PygmentsHighlighter):'
29 29 def highlightBlock(self, qstring):
30 30 """ Highlight a block of text. Reimplemented to highlight selectively.
31 31 """
32 if self.highlighting_on:
33 for prompt in (self._frontend._continuation_prompt,
34 self._frontend._prompt):
35 if qstring.startsWith(prompt):
36 qstring.remove(0, len(prompt))
37 self._current_offset = len(prompt)
38 break
39 PygmentsHighlighter.highlightBlock(self, qstring)
32 if not self.highlighting_on:
33 return
34
35 # The input to this function is unicode string that may contain
36 # paragraph break characters, non-breaking spaces, etc. Here we acquire
37 # the string as plain text so we can compare it.
38 current_block = self.currentBlock()
39 string = self._frontend._get_block_plain_text(current_block)
40
41 # Decide whether to check for the regular or continuation prompt.
42 if current_block.contains(self._frontend._prompt_pos):
43 prompt = self._frontend._prompt
44 else:
45 prompt = self._frontend._continuation_prompt
46
47 # Don't highlight the part of the string that contains the prompt.
48 if string.startswith(prompt):
49 self._current_offset = len(prompt)
50 qstring.remove(0, len(prompt))
51 else:
52 self._current_offset = 0
53
54 PygmentsHighlighter.highlightBlock(self, qstring)
40 55
41 56 def setFormat(self, start, count, format):
42 """ Reimplemented to avoid highlighting continuation prompts.
57 """ Reimplemented to highlight selectively.
43 58 """
44 59 start += self._current_offset
45 60 PygmentsHighlighter.setFormat(self, start, count, format)
@@ -59,9 +74,6 b' class FrontendWidget(HistoryConsoleWidget):'
59 74 def __init__(self, parent=None):
60 75 super(FrontendWidget, self).__init__(parent)
61 76
62 # ConsoleWidget protected variables.
63 self._continuation_prompt = '... '
64
65 77 # FrontendWidget protected variables.
66 78 self._call_tip_widget = CallTipWidget(self)
67 79 self._completion_lexer = CompletionLexer(PythonLexer())
@@ -70,6 +82,9 b' class FrontendWidget(HistoryConsoleWidget):'
70 82 self._input_splitter = InputSplitter(input_mode='replace')
71 83 self._kernel_manager = None
72 84
85 # Configure the ConsoleWidget.
86 self._set_continuation_prompt('... ')
87
73 88 self.document().contentsChange.connect(self._document_contents_change)
74 89
75 90 #---------------------------------------------------------------------------
@@ -143,17 +158,6 b' class FrontendWidget(HistoryConsoleWidget):'
143 158 return False
144 159
145 160 #---------------------------------------------------------------------------
146 # 'ConsoleWidget' protected interface
147 #---------------------------------------------------------------------------
148
149 def _show_prompt(self, prompt=None, newline=True):
150 """ Reimplemented to set a default prompt.
151 """
152 if prompt is None:
153 prompt = '>>> '
154 super(FrontendWidget, self)._show_prompt(prompt, newline)
155
156 #---------------------------------------------------------------------------
157 161 # 'FrontendWidget' interface
158 162 #---------------------------------------------------------------------------
159 163
@@ -283,20 +287,24 b' class FrontendWidget(HistoryConsoleWidget):'
283 287 self.appendPlainText('Kernel process is either remote or '
284 288 'unspecified. Cannot interrupt.\n')
285 289
290 def _show_interpreter_prompt(self):
291 """ Shows a prompt for the interpreter.
292 """
293 self._show_prompt('>>> ')
294
286 295 #------ Signal handlers ----------------------------------------------------
287 296
288 297 def _started_channels(self):
289 298 """ Called when the kernel manager has started listening.
290 299 """
291 QtGui.QPlainTextEdit.clear(self)
292 if self._reading:
293 self._reading = False
300 self._reset()
294 301 self.appendPlainText(self._get_banner())
295 self._show_prompt()
302 self._show_interpreter_prompt()
296 303
297 304 def _stopped_channels(self):
298 305 """ Called when the kernel manager has stopped listening.
299 306 """
307 # FIXME: Print a message here?
300 308 pass
301 309
302 310 def _document_contents_change(self, position, removed, added):
@@ -327,9 +335,7 b' class FrontendWidget(HistoryConsoleWidget):'
327 335 handler(omsg)
328 336
329 337 def _handle_pyout(self, omsg):
330 session = omsg['parent_header']['session']
331 if session == self.kernel_manager.session.session:
332 self.appendPlainText(omsg['content']['data'] + '\n')
338 self.appendPlainText(omsg['content']['data'] + '\n')
333 339
334 340 def _handle_stream(self, omsg):
335 341 self.appendPlainText(omsg['content']['data'])
@@ -351,7 +357,7 b' class FrontendWidget(HistoryConsoleWidget):'
351 357 text = "ERROR: ABORTED\n"
352 358 self.appendPlainText(text)
353 359 self._hidden = True
354 self._show_prompt()
360 self._show_interpreter_prompt()
355 361 self.executed.emit(rep)
356 362
357 363 def _handle_complete_reply(self, rep):
@@ -10,6 +10,14 b' class IPythonWidget(FrontendWidget):'
10 10 """ A FrontendWidget for an IPython kernel.
11 11 """
12 12
13 # The default stylesheet for prompts, colors, etc.
14 default_stylesheet = """
15 .in-prompt { color: navy; }
16 .in-prompt-number { font-weight: bold; }
17 .out-prompt { color: darkred; }
18 .out-prompt-number { font-weight: bold; }
19 """
20
13 21 #---------------------------------------------------------------------------
14 22 # 'QObject' interface
15 23 #---------------------------------------------------------------------------
@@ -17,7 +25,12 b' class IPythonWidget(FrontendWidget):'
17 25 def __init__(self, parent=None):
18 26 super(IPythonWidget, self).__init__(parent)
19 27
28 # Initialize protected variables.
20 29 self._magic_overrides = {}
30 self._prompt_count = 0
31
32 # Set a default stylesheet.
33 self.set_style_sheet(self.default_stylesheet)
21 34
22 35 #---------------------------------------------------------------------------
23 36 # 'ConsoleWidget' abstract interface
@@ -38,7 +51,7 b' class IPythonWidget(FrontendWidget):'
38 51 output = callback(arguments)
39 52 if output:
40 53 self.appendPlainText(output)
41 self._show_prompt()
54 self._show_interpreter_prompt()
42 55 else:
43 56 super(IPythonWidget, self)._execute(source, hidden)
44 57
@@ -56,10 +69,36 b' class IPythonWidget(FrontendWidget):'
56 69 #---------------------------------------------------------------------------
57 70
58 71 def _get_banner(self):
59 """ Reimplemented to a return IPython's default banner.
72 """ Reimplemented to return IPython's default banner.
60 73 """
61 74 return default_banner
62 75
76 def _show_interpreter_prompt(self):
77 """ Reimplemented for IPython-style prompts.
78 """
79 self._prompt_count += 1
80 prompt_template = '<span class="in-prompt">%s</span>'
81 prompt_body = '<br/>In [<span class="in-prompt-number">%i</span>]: '
82 prompt = (prompt_template % prompt_body) % self._prompt_count
83 self._show_prompt(prompt, html=True)
84
85 # Update continuation prompt to reflect (possibly) new prompt length.
86 cont_prompt_chars = '...: '
87 space_count = len(self._prompt.lstrip()) - len(cont_prompt_chars)
88 cont_prompt_body = '&nbsp;' * space_count + cont_prompt_chars
89 self._continuation_prompt_html = prompt_template % cont_prompt_body
90
91 #------ Signal handlers ----------------------------------------------------
92
93 def _handle_pyout(self, omsg):
94 """ Reimplemented for IPython-style "display hook".
95 """
96 prompt_template = '<span class="out-prompt">%s</span>'
97 prompt_body = 'Out[<span class="out-prompt-number">%i</span>]: '
98 prompt = (prompt_template % prompt_body) % self._prompt_count
99 self.appendHtml(prompt)
100 self.appendPlainText(omsg['content']['data'] + '\n')
101
63 102 #---------------------------------------------------------------------------
64 103 # 'IPythonWidget' interface
65 104 #---------------------------------------------------------------------------
@@ -81,6 +120,11 b' class IPythonWidget(FrontendWidget):'
81 120 except KeyError:
82 121 pass
83 122
123 def set_style_sheet(self, stylesheet):
124 """ Sets the style sheet.
125 """
126 self.document().setDefaultStyleSheet(stylesheet)
127
84 128
85 129 if __name__ == '__main__':
86 130 from IPython.frontend.qt.kernelmanager import QtKernelManager
@@ -1,7 +1,7 b''
1 1 # System library imports.
2 2 from PyQt4 import QtGui
3 3 from pygments.lexer import RegexLexer, _TokenType, Text, Error
4 from pygments.lexers import CLexer, CppLexer, PythonLexer
4 from pygments.lexers import PythonLexer
5 5 from pygments.styles.default import DefaultStyle
6 6 from pygments.token import Comment
7 7
@@ -133,7 +133,7 b' class PygmentsHighlighter(QtGui.QSyntaxHighlighter):'
133 133 if token in self._formats:
134 134 return self._formats[token]
135 135 result = None
136 for key, value in self._style.style_for_token(token) .items():
136 for key, value in self._style.style_for_token(token).items():
137 137 if value:
138 138 if result is None:
139 139 result = QtGui.QTextCharFormat()
@@ -171,12 +171,11 b' class PygmentsHighlighter(QtGui.QSyntaxHighlighter):'
171 171 qcolor = self._get_color(color)
172 172 result = QtGui.QBrush(qcolor)
173 173 self._brushes[color] = result
174
175 174 return result
176 175
177 176 def _get_color(self, color):
178 177 qcolor = QtGui.QColor()
179 qcolor.setRgb(int(color[:2],base=16),
178 qcolor.setRgb(int(color[:2], base=16),
180 179 int(color[2:4], base=16),
181 180 int(color[4:6], base=16))
182 181 return qcolor
General Comments 0
You need to be logged in to leave comments. Login now