##// END OF EJS Templates
Merge pull request #526 from epatters/append-before-prompt...
Evan Patterson -
r4065:889e008b merge
parent child Browse files
Show More
@@ -65,13 +65,16 b' class ConsoleWidget(Configurable, QtGui.QWidget):'
65 """
65 """
66 )
66 )
67 gui_completion = Bool(False, config=True,
67 gui_completion = Bool(False, config=True,
68 help="Use a list widget instead of plain text output for tab completion."
68 help="""
69 Use a list widget instead of plain text output for tab completion.
70 """
69 )
71 )
70 # NOTE: this value can only be specified during initialization.
72 # NOTE: this value can only be specified during initialization.
71 kind = Enum(['plain', 'rich'], default_value='plain', config=True,
73 kind = Enum(['plain', 'rich'], default_value='plain', config=True,
72 help="""
74 help="""
73 The type of underlying text widget to use. Valid values are 'plain', which
75 The type of underlying text widget to use. Valid values are 'plain',
74 specifies a QPlainTextEdit, and 'rich', which specifies a QTextEdit.
76 which specifies a QPlainTextEdit, and 'rich', which specifies a
77 QTextEdit.
75 """
78 """
76 )
79 )
77 # NOTE: this value can only be specified during initialization.
80 # NOTE: this value can only be specified during initialization.
@@ -84,7 +87,8 b' class ConsoleWidget(Configurable, QtGui.QWidget):'
84 'hsplit' : When paging is requested, the widget is split
87 'hsplit' : When paging is requested, the widget is split
85 horizontally. The top pane contains the console, and the
88 horizontally. The top pane contains the console, and the
86 bottom pane contains the paged text.
89 bottom pane contains the paged text.
87 'vsplit' : Similar to 'hsplit', except that a vertical splitter used.
90 'vsplit' : Similar to 'hsplit', except that a vertical splitter
91 used.
88 'custom' : No action is taken by the widget beyond emitting a
92 'custom' : No action is taken by the widget beyond emitting a
89 'custom_page_requested(str)' signal.
93 'custom_page_requested(str)' signal.
90 'none' : The text is written directly to the console.
94 'none' : The text is written directly to the console.
@@ -195,6 +199,7 b' class ConsoleWidget(Configurable, QtGui.QWidget):'
195
199
196 # Initialize protected variables. Some variables contain useful state
200 # Initialize protected variables. Some variables contain useful state
197 # information for subclasses; they should be considered read-only.
201 # information for subclasses; they should be considered read-only.
202 self._append_before_prompt_pos = 0
198 self._ansi_processor = QtAnsiCodeProcessor()
203 self._ansi_processor = QtAnsiCodeProcessor()
199 self._completion_widget = CompletionWidget(self._control)
204 self._completion_widget = CompletionWidget(self._control)
200 self._continuation_prompt = '> '
205 self._continuation_prompt = '> '
@@ -507,7 +512,7 b' class ConsoleWidget(Configurable, QtGui.QWidget):'
507 """
512 """
508 self._html_exporter.export()
513 self._html_exporter.export()
509
514
510 def _get_input_buffer(self):
515 def _get_input_buffer(self, force=False):
511 """ The text that the user has entered entered at the current prompt.
516 """ The text that the user has entered entered at the current prompt.
512
517
513 If the console is currently executing, the text that is executing will
518 If the console is currently executing, the text that is executing will
@@ -515,7 +520,7 b' class ConsoleWidget(Configurable, QtGui.QWidget):'
515 """
520 """
516 # If we're executing, the input buffer may not even exist anymore due to
521 # If we're executing, the input buffer may not even exist anymore due to
517 # the limit imposed by 'buffer_size'. Therefore, we store it.
522 # the limit imposed by 'buffer_size'. Therefore, we store it.
518 if self._executing:
523 if self._executing and not force:
519 return self._input_buffer_executing
524 return self._input_buffer_executing
520
525
521 cursor = self._get_end_cursor()
526 cursor = self._get_end_cursor()
@@ -718,36 +723,47 b' class ConsoleWidget(Configurable, QtGui.QWidget):'
718 # 'ConsoleWidget' protected interface
723 # 'ConsoleWidget' protected interface
719 #--------------------------------------------------------------------------
724 #--------------------------------------------------------------------------
720
725
721 def _append_html(self, html):
726 def _append_custom(self, insert, input, before_prompt=False):
722 """ Appends html at the end of the console buffer.
727 """ A low-level method for appending content to the end of the buffer.
728
729 If 'before_prompt' is enabled, the content will be inserted before the
730 current prompt, if there is one.
723 """
731 """
724 cursor = self._get_end_cursor()
732 # Determine where to insert the content.
725 self._insert_html(cursor, html)
733 cursor = self._control.textCursor()
734 if before_prompt and not self._executing:
735 cursor.setPosition(self._append_before_prompt_pos)
736 else:
737 cursor.movePosition(QtGui.QTextCursor.End)
738 start_pos = cursor.position()
726
739
727 def _append_html_fetching_plain_text(self, html):
740 # Perform the insertion.
728 """ Appends 'html', then returns the plain text version of it.
741 result = insert(cursor, input)
729 """
742
730 cursor = self._get_end_cursor()
743 # Adjust the prompt position if we have inserted before it. This is safe
731 return self._insert_html_fetching_plain_text(cursor, html)
744 # because buffer truncation is disabled when not executing.
745 if before_prompt and not self._executing:
746 diff = cursor.position() - start_pos
747 self._append_before_prompt_pos += diff
748 self._prompt_pos += diff
749
750 return result
732
751
733 def _append_plain_text(self, text):
752 def _append_html(self, html, before_prompt=False):
734 """ Appends plain text at the end of the console buffer, processing
753 """ Appends HTML at the end of the console buffer.
735 ANSI codes if enabled.
736 """
754 """
737 cursor = self._get_end_cursor()
755 self._append_custom(self._insert_html, html, before_prompt)
738 self._insert_plain_text(cursor, text)
739
756
740 def _append_plain_text_keeping_prompt(self, text):
757 def _append_html_fetching_plain_text(self, html, before_prompt=False):
741 """ Writes 'text' after the current prompt, then restores the old prompt
758 """ Appends HTML, then returns the plain text version of it.
742 with its old input buffer.
743 """
759 """
744 input_buffer = self.input_buffer
760 return self._append_custom(self._insert_html_fetching_plain_text,
745 self._append_plain_text('\n')
761 html, before_prompt)
746 self._prompt_finished()
747
762
748 self._append_plain_text(text)
763 def _append_plain_text(self, text, before_prompt=False):
749 self._show_prompt()
764 """ Appends plain text, processing ANSI codes if enabled.
750 self.input_buffer = input_buffer
765 """
766 self._append_custom(self._insert_plain_text, text, before_prompt)
751
767
752 def _cancel_text_completion(self):
768 def _cancel_text_completion(self):
753 """ If text completion is progress, cancel it.
769 """ If text completion is progress, cancel it.
@@ -1616,7 +1632,8 b' class ConsoleWidget(Configurable, QtGui.QWidget):'
1616 self._control.setReadOnly(False)
1632 self._control.setReadOnly(False)
1617 self._control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1633 self._control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1618
1634
1619 self._executing = False
1635 if not self._reading:
1636 self._executing = False
1620 self._prompt_started_hook()
1637 self._prompt_started_hook()
1621
1638
1622 # If the input buffer has changed while executing, load it.
1639 # If the input buffer has changed while executing, load it.
@@ -1659,11 +1676,11 b' class ConsoleWidget(Configurable, QtGui.QWidget):'
1659 self._reading_callback = None
1676 self._reading_callback = None
1660 while self._reading:
1677 while self._reading:
1661 QtCore.QCoreApplication.processEvents()
1678 QtCore.QCoreApplication.processEvents()
1662 return self.input_buffer.rstrip('\n')
1679 return self._get_input_buffer(force=True).rstrip('\n')
1663
1680
1664 else:
1681 else:
1665 self._reading_callback = lambda: \
1682 self._reading_callback = lambda: \
1666 callback(self.input_buffer.rstrip('\n'))
1683 callback(self._get_input_buffer(force=True).rstrip('\n'))
1667
1684
1668 def _set_continuation_prompt(self, prompt, html=False):
1685 def _set_continuation_prompt(self, prompt, html=False):
1669 """ Sets the continuation prompt.
1686 """ Sets the continuation prompt.
@@ -1716,14 +1733,16 b' class ConsoleWidget(Configurable, QtGui.QWidget):'
1716 If set, a new line will be written before showing the prompt if
1733 If set, a new line will be written before showing the prompt if
1717 there is not already a newline at the end of the buffer.
1734 there is not already a newline at the end of the buffer.
1718 """
1735 """
1736 # Save the current end position to support _append*(before_prompt=True).
1737 cursor = self._get_end_cursor()
1738 self._append_before_prompt_pos = cursor.position()
1739
1719 # Insert a preliminary newline, if necessary.
1740 # Insert a preliminary newline, if necessary.
1720 if newline:
1741 if newline and cursor.position() > 0:
1721 cursor = self._get_end_cursor()
1742 cursor.movePosition(QtGui.QTextCursor.Left,
1722 if cursor.position() > 0:
1743 QtGui.QTextCursor.KeepAnchor)
1723 cursor.movePosition(QtGui.QTextCursor.Left,
1744 if cursor.selection().toPlainText() != '\n':
1724 QtGui.QTextCursor.KeepAnchor)
1745 self._append_plain_text('\n')
1725 if cursor.selection().toPlainText() != '\n':
1726 self._append_plain_text('\n')
1727
1746
1728 # Write the prompt.
1747 # Write the prompt.
1729 self._append_plain_text(self._prompt_sep)
1748 self._append_plain_text(self._prompt_sep)
@@ -22,8 +22,7 b' from pygments_highlighter import PygmentsHighlighter'
22
22
23
23
24 class FrontendHighlighter(PygmentsHighlighter):
24 class FrontendHighlighter(PygmentsHighlighter):
25 """ A PygmentsHighlighter that can be turned on and off and that ignores
25 """ A PygmentsHighlighter that understands and ignores prompts.
26 prompts.
27 """
26 """
28
27
29 def __init__(self, frontend):
28 def __init__(self, frontend):
@@ -50,14 +49,12 b' class FrontendHighlighter(PygmentsHighlighter):'
50 else:
49 else:
51 prompt = self._frontend._continuation_prompt
50 prompt = self._frontend._continuation_prompt
52
51
53 # Don't highlight the part of the string that contains the prompt.
52 # Only highlight if we can identify a prompt, but make sure not to
53 # highlight the prompt.
54 if string.startswith(prompt):
54 if string.startswith(prompt):
55 self._current_offset = len(prompt)
55 self._current_offset = len(prompt)
56 string = string[len(prompt):]
56 string = string[len(prompt):]
57 else:
57 super(FrontendHighlighter, self).highlightBlock(string)
58 self._current_offset = 0
59
60 PygmentsHighlighter.highlightBlock(self, string)
61
58
62 def rehighlightBlock(self, block):
59 def rehighlightBlock(self, block):
63 """ Reimplemented to temporarily enable highlighting if disabled.
60 """ Reimplemented to temporarily enable highlighting if disabled.
@@ -71,7 +68,7 b' class FrontendHighlighter(PygmentsHighlighter):'
71 """ Reimplemented to highlight selectively.
68 """ Reimplemented to highlight selectively.
72 """
69 """
73 start += self._current_offset
70 start += self._current_offset
74 PygmentsHighlighter.setFormat(self, start, count, format)
71 super(FrontendHighlighter, self).setFormat(start, count, format)
75
72
76
73
77 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
74 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
@@ -372,14 +369,8 b' class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):'
372 """ Handle display hook output.
369 """ Handle display hook output.
373 """
370 """
374 if not self._hidden and self._is_from_this_session(msg):
371 if not self._hidden and self._is_from_this_session(msg):
375 data = msg['content']['data']
372 text = msg['content']['data']
376 if isinstance(data, basestring):
373 self._append_plain_text(text + '\n', before_prompt=True)
377 # plaintext data from pure Python kernel
378 text = data
379 else:
380 # formatted output from DisplayFormatter (IPython kernel)
381 text = data.get('text/plain', '')
382 self._append_plain_text(text + '\n')
383
374
384 def _handle_stream(self, msg):
375 def _handle_stream(self, msg):
385 """ Handle stdout, stderr, and stdin.
376 """ Handle stdout, stderr, and stdin.
@@ -390,7 +381,7 b' class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):'
390 # widget's tab width.
381 # widget's tab width.
391 text = msg['content']['data'].expandtabs(8)
382 text = msg['content']['data'].expandtabs(8)
392
383
393 self._append_plain_text(text)
384 self._append_plain_text(text, before_prompt=True)
394 self._control.moveCursor(QtGui.QTextCursor.End)
385 self._control.moveCursor(QtGui.QTextCursor.End)
395
386
396 def _handle_shutdown_reply(self, msg):
387 def _handle_shutdown_reply(self, msg):
@@ -62,16 +62,16 b' class IPythonWidget(FrontendWidget):'
62 editor = Unicode(default_editor, config=True,
62 editor = Unicode(default_editor, config=True,
63 help="""
63 help="""
64 A command for invoking a system text editor. If the string contains a
64 A command for invoking a system text editor. If the string contains a
65 {filename} format specifier, it will be used. Otherwise, the filename will
65 {filename} format specifier, it will be used. Otherwise, the filename
66 be appended to the end the command.
66 will be appended to the end the command.
67 """)
67 """)
68
68
69 editor_line = Unicode(config=True,
69 editor_line = Unicode(config=True,
70 help="""
70 help="""
71 The editor command to use when a specific line number is requested. The
71 The editor command to use when a specific line number is requested. The
72 string should contain two format specifiers: {line} and {filename}. If
72 string should contain two format specifiers: {line} and {filename}. If
73 this parameter is not specified, the line number option to the %edit magic
73 this parameter is not specified, the line number option to the %edit
74 will be ignored.
74 magic will be ignored.
75 """)
75 """)
76
76
77 style_sheet = Unicode(config=True,
77 style_sheet = Unicode(config=True,
@@ -85,8 +85,9 b' class IPythonWidget(FrontendWidget):'
85
85
86 syntax_style = Str(config=True,
86 syntax_style = Str(config=True,
87 help="""
87 help="""
88 If not empty, use this Pygments style for syntax highlighting. Otherwise,
88 If not empty, use this Pygments style for syntax highlighting.
89 the style sheet is queried for Pygments style information.
89 Otherwise, the style sheet is queried for Pygments style
90 information.
90 """)
91 """)
91
92
92 # Prompts.
93 # Prompts.
@@ -187,20 +188,20 b' class IPythonWidget(FrontendWidget):'
187 prompt_number = content['execution_count']
188 prompt_number = content['execution_count']
188 data = content['data']
189 data = content['data']
189 if data.has_key('text/html'):
190 if data.has_key('text/html'):
190 self._append_plain_text(self.output_sep)
191 self._append_plain_text(self.output_sep, True)
191 self._append_html(self._make_out_prompt(prompt_number))
192 self._append_html(self._make_out_prompt(prompt_number), True)
192 html = data['text/html']
193 html = data['text/html']
193 self._append_plain_text('\n')
194 self._append_plain_text('\n', True)
194 self._append_html(html + self.output_sep2)
195 self._append_html(html + self.output_sep2, True)
195 elif data.has_key('text/plain'):
196 elif data.has_key('text/plain'):
196 self._append_plain_text(self.output_sep)
197 self._append_plain_text(self.output_sep, True)
197 self._append_html(self._make_out_prompt(prompt_number))
198 self._append_html(self._make_out_prompt(prompt_number), True)
198 text = data['text/plain']
199 text = data['text/plain']
199 # If the repr is multiline, make sure we start on a new line,
200 # If the repr is multiline, make sure we start on a new line,
200 # so that its lines are aligned.
201 # so that its lines are aligned.
201 if "\n" in text and not self.output_sep.endswith("\n"):
202 if "\n" in text and not self.output_sep.endswith("\n"):
202 self._append_plain_text('\n')
203 self._append_plain_text('\n', True)
203 self._append_plain_text(text + self.output_sep2)
204 self._append_plain_text(text + self.output_sep2, True)
204
205
205 def _handle_display_data(self, msg):
206 def _handle_display_data(self, msg):
206 """ The base handler for the ``display_data`` message.
207 """ The base handler for the ``display_data`` message.
@@ -216,19 +217,19 b' class IPythonWidget(FrontendWidget):'
216 # representation.
217 # representation.
217 if data.has_key('text/html'):
218 if data.has_key('text/html'):
218 html = data['text/html']
219 html = data['text/html']
219 self._append_html(html)
220 self._append_html(html, True)
220 elif data.has_key('text/plain'):
221 elif data.has_key('text/plain'):
221 text = data['text/plain']
222 text = data['text/plain']
222 self._append_plain_text(text)
223 self._append_plain_text(text, True)
223 # This newline seems to be needed for text and html output.
224 # This newline seems to be needed for text and html output.
224 self._append_plain_text(u'\n')
225 self._append_plain_text(u'\n', True)
225
226
226 def _started_channels(self):
227 def _started_channels(self):
227 """ Reimplemented to make a history request.
228 """ Reimplemented to make a history request.
228 """
229 """
229 super(IPythonWidget, self)._started_channels()
230 super(IPythonWidget, self)._started_channels()
230 self.kernel_manager.shell_channel.history(hist_access_type='tail', n=1000)
231 self.kernel_manager.shell_channel.history(hist_access_type='tail',
231
232 n=1000)
232 #---------------------------------------------------------------------------
233 #---------------------------------------------------------------------------
233 # 'ConsoleWidget' public interface
234 # 'ConsoleWidget' public interface
234 #---------------------------------------------------------------------------
235 #---------------------------------------------------------------------------
@@ -413,8 +414,8 b' class IPythonWidget(FrontendWidget):'
413 self.custom_edit_requested.emit(filename, line)
414 self.custom_edit_requested.emit(filename, line)
414 elif not self.editor:
415 elif not self.editor:
415 self._append_plain_text('No default editor available.\n'
416 self._append_plain_text('No default editor available.\n'
416 'Specify a GUI text editor in the `IPythonWidget.editor` configurable\n'
417 'Specify a GUI text editor in the `IPythonWidget.editor` '
417 'to enable the %edit magic')
418 'configurable to enable the %edit magic')
418 else:
419 else:
419 try:
420 try:
420 filename = '"%s"' % filename
421 filename = '"%s"' % filename
@@ -74,20 +74,17 b' class RichIPythonWidget(IPythonWidget):'
74 prompt_number = content['execution_count']
74 prompt_number = content['execution_count']
75 data = content['data']
75 data = content['data']
76 if data.has_key('image/svg+xml'):
76 if data.has_key('image/svg+xml'):
77 self._append_plain_text(self.output_sep)
77 self._append_plain_text(self.output_sep, True)
78 self._append_html(self._make_out_prompt(prompt_number))
78 self._append_html(self._make_out_prompt(prompt_number), True)
79 # TODO: try/except this call.
79 self._append_svg(data['image/svg+xml'], True)
80 self._append_svg(data['image/svg+xml'])
80 self._append_html(self.output_sep2, True)
81 self._append_html(self.output_sep2)
82 elif data.has_key('image/png'):
81 elif data.has_key('image/png'):
83 self._append_plain_text(self.output_sep)
82 self._append_plain_text(self.output_sep, True)
84 self._append_html(self._make_out_prompt(prompt_number))
83 self._append_html(self._make_out_prompt(prompt_number), True)
85 # This helps the output to look nice.
84 # This helps the output to look nice.
86 self._append_plain_text('\n')
85 self._append_plain_text('\n', True)
87 # TODO: try/except these calls
86 self._append_png(decodestring(data['image/png']), True)
88 png = decodestring(data['image/png'])
87 self._append_html(self.output_sep2, True)
89 self._append_png(png)
90 self._append_html(self.output_sep2)
91 else:
88 else:
92 # Default back to the plain text representation.
89 # Default back to the plain text representation.
93 return super(RichIPythonWidget, self)._handle_pyout(msg)
90 return super(RichIPythonWidget, self)._handle_pyout(msg)
@@ -103,14 +100,12 b' class RichIPythonWidget(IPythonWidget):'
103 # FIXME: Is this the right ordering of things to try?
100 # FIXME: Is this the right ordering of things to try?
104 if data.has_key('image/svg+xml'):
101 if data.has_key('image/svg+xml'):
105 svg = data['image/svg+xml']
102 svg = data['image/svg+xml']
106 # TODO: try/except this call.
103 self._append_svg(svg, True)
107 self._append_svg(svg)
108 elif data.has_key('image/png'):
104 elif data.has_key('image/png'):
109 # TODO: try/except these calls
110 # PNG data is base64 encoded as it passes over the network
105 # PNG data is base64 encoded as it passes over the network
111 # in a JSON structure so we decode it.
106 # in a JSON structure so we decode it.
112 png = decodestring(data['image/png'])
107 png = decodestring(data['image/png'])
113 self._append_png(png)
108 self._append_png(png, True)
114 else:
109 else:
115 # Default back to the plain text representation.
110 # Default back to the plain text representation.
116 return super(RichIPythonWidget, self)._handle_display_data(msg)
111 return super(RichIPythonWidget, self)._handle_display_data(msg)
@@ -119,35 +114,15 b' class RichIPythonWidget(IPythonWidget):'
119 # 'RichIPythonWidget' protected interface
114 # 'RichIPythonWidget' protected interface
120 #---------------------------------------------------------------------------
115 #---------------------------------------------------------------------------
121
116
122 def _append_svg(self, svg):
117 def _append_png(self, png, before_prompt=False):
123 """ Append raw svg data to the widget.
118 """ Append raw PNG data to the widget.
124 """
119 """
125 try:
120 self._append_custom(self._insert_png, png, before_prompt)
126 image = svg_to_image(svg)
127 except ValueError:
128 self._append_plain_text('Received invalid plot data.')
129 else:
130 format = self._add_image(image)
131 self._name_to_svg_map[format.name()] = svg
132 cursor = self._get_end_cursor()
133 cursor.insertBlock()
134 cursor.insertImage(format)
135 cursor.insertBlock()
136
121
137 def _append_png(self, png):
122 def _append_svg(self, svg, before_prompt=False):
138 """ Append raw svg data to the widget.
123 """ Append raw SVG data to the widget.
139 """
124 """
140 try:
125 self._append_custom(self._insert_svg, svg, before_prompt)
141 image = QtGui.QImage()
142 image.loadFromData(png, 'PNG')
143 except ValueError:
144 self._append_plain_text('Received invalid plot data.')
145 else:
146 format = self._add_image(image)
147 cursor = self._get_end_cursor()
148 cursor.insertBlock()
149 cursor.insertImage(format)
150 cursor.insertBlock()
151
126
152 def _add_image(self, image):
127 def _add_image(self, image):
153 """ Adds the specified QImage to the document and returns a
128 """ Adds the specified QImage to the document and returns a
@@ -236,6 +211,34 b' class RichIPythonWidget(IPythonWidget):'
236 else:
211 else:
237 return '<b>Unrecognized image format</b>'
212 return '<b>Unrecognized image format</b>'
238
213
214 def _insert_png(self, cursor, png):
215 """ Insert raw PNG data into the widget.
216 """
217 try:
218 image = QtGui.QImage()
219 image.loadFromData(png, 'PNG')
220 except ValueError:
221 self._insert_plain_text(cursor, 'Received invalid PNG data.')
222 else:
223 format = self._add_image(image)
224 cursor.insertBlock()
225 cursor.insertImage(format)
226 cursor.insertBlock()
227
228 def _insert_svg(self, cursor, svg):
229 """ Insert raw SVG data into the widet.
230 """
231 try:
232 image = svg_to_image(svg)
233 except ValueError:
234 self._insert_plain_text(cursor, 'Received invalid SVG data.')
235 else:
236 format = self._add_image(image)
237 self._name_to_svg_map[format.name()] = svg
238 cursor.insertBlock()
239 cursor.insertImage(format)
240 cursor.insertBlock()
241
239 def _save_image(self, name, format='PNG'):
242 def _save_image(self, name, format='PNG'):
240 """ Shows a save dialog for the ImageResource with 'name'.
243 """ Shows a save dialog for the ImageResource with 'name'.
241 """
244 """
General Comments 0
You need to be logged in to leave comments. Login now