##// END OF EJS Templates
Make qtconsole aware of clear_output.
Jonathan Frederic -
Show More
@@ -1,806 +1,838 b''
1 from __future__ import print_function
1 from __future__ import print_function
2
2
3 # Standard library imports
3 # Standard library imports
4 from collections import namedtuple
4 from collections import namedtuple
5 import sys
5 import sys
6 import uuid
6 import uuid
7
7
8 # System library imports
8 # System library imports
9 from IPython.external import qt
9 from IPython.external import qt
10 from IPython.external.qt import QtCore, QtGui
10 from IPython.external.qt import QtCore, QtGui
11 from IPython.utils import py3compat
11 from IPython.utils import py3compat
12 from IPython.utils.importstring import import_item
12 from IPython.utils.importstring import import_item
13
13
14 # Local imports
14 # Local imports
15 from IPython.core.inputsplitter import InputSplitter, IPythonInputSplitter
15 from IPython.core.inputsplitter import InputSplitter, IPythonInputSplitter
16 from IPython.core.inputtransformer import classic_prompt
16 from IPython.core.inputtransformer import classic_prompt
17 from IPython.core.oinspect import call_tip
17 from IPython.core.oinspect import call_tip
18 from IPython.qt.base_frontend_mixin import BaseFrontendMixin
18 from IPython.qt.base_frontend_mixin import BaseFrontendMixin
19 from IPython.utils.traitlets import Any, Bool, Instance, Unicode, DottedObjectName
19 from IPython.utils.traitlets import Any, Bool, Instance, Unicode, DottedObjectName
20 from .bracket_matcher import BracketMatcher
20 from .bracket_matcher import BracketMatcher
21 from .call_tip_widget import CallTipWidget
21 from .call_tip_widget import CallTipWidget
22 from .completion_lexer import CompletionLexer
22 from .completion_lexer import CompletionLexer
23 from .history_console_widget import HistoryConsoleWidget
23 from .history_console_widget import HistoryConsoleWidget
24 from .pygments_highlighter import PygmentsHighlighter
24 from .pygments_highlighter import PygmentsHighlighter
25
25
26
26
27 class FrontendHighlighter(PygmentsHighlighter):
27 class FrontendHighlighter(PygmentsHighlighter):
28 """ A PygmentsHighlighter that understands and ignores prompts.
28 """ A PygmentsHighlighter that understands and ignores prompts.
29 """
29 """
30
30
31 def __init__(self, frontend, lexer=None):
31 def __init__(self, frontend, lexer=None):
32 super(FrontendHighlighter, self).__init__(frontend._control.document(), lexer=lexer)
32 super(FrontendHighlighter, self).__init__(frontend._control.document(), lexer=lexer)
33 self._current_offset = 0
33 self._current_offset = 0
34 self._frontend = frontend
34 self._frontend = frontend
35 self.highlighting_on = False
35 self.highlighting_on = False
36
36
37 def highlightBlock(self, string):
37 def highlightBlock(self, string):
38 """ Highlight a block of text. Reimplemented to highlight selectively.
38 """ Highlight a block of text. Reimplemented to highlight selectively.
39 """
39 """
40 if not self.highlighting_on:
40 if not self.highlighting_on:
41 return
41 return
42
42
43 # The input to this function is a unicode string that may contain
43 # The input to this function is a unicode string that may contain
44 # paragraph break characters, non-breaking spaces, etc. Here we acquire
44 # paragraph break characters, non-breaking spaces, etc. Here we acquire
45 # the string as plain text so we can compare it.
45 # the string as plain text so we can compare it.
46 current_block = self.currentBlock()
46 current_block = self.currentBlock()
47 string = self._frontend._get_block_plain_text(current_block)
47 string = self._frontend._get_block_plain_text(current_block)
48
48
49 # Decide whether to check for the regular or continuation prompt.
49 # Decide whether to check for the regular or continuation prompt.
50 if current_block.contains(self._frontend._prompt_pos):
50 if current_block.contains(self._frontend._prompt_pos):
51 prompt = self._frontend._prompt
51 prompt = self._frontend._prompt
52 else:
52 else:
53 prompt = self._frontend._continuation_prompt
53 prompt = self._frontend._continuation_prompt
54
54
55 # Only highlight if we can identify a prompt, but make sure not to
55 # Only highlight if we can identify a prompt, but make sure not to
56 # highlight the prompt.
56 # highlight the prompt.
57 if string.startswith(prompt):
57 if string.startswith(prompt):
58 self._current_offset = len(prompt)
58 self._current_offset = len(prompt)
59 string = string[len(prompt):]
59 string = string[len(prompt):]
60 super(FrontendHighlighter, self).highlightBlock(string)
60 super(FrontendHighlighter, self).highlightBlock(string)
61
61
62 def rehighlightBlock(self, block):
62 def rehighlightBlock(self, block):
63 """ Reimplemented to temporarily enable highlighting if disabled.
63 """ Reimplemented to temporarily enable highlighting if disabled.
64 """
64 """
65 old = self.highlighting_on
65 old = self.highlighting_on
66 self.highlighting_on = True
66 self.highlighting_on = True
67 super(FrontendHighlighter, self).rehighlightBlock(block)
67 super(FrontendHighlighter, self).rehighlightBlock(block)
68 self.highlighting_on = old
68 self.highlighting_on = old
69
69
70 def setFormat(self, start, count, format):
70 def setFormat(self, start, count, format):
71 """ Reimplemented to highlight selectively.
71 """ Reimplemented to highlight selectively.
72 """
72 """
73 start += self._current_offset
73 start += self._current_offset
74 super(FrontendHighlighter, self).setFormat(start, count, format)
74 super(FrontendHighlighter, self).setFormat(start, count, format)
75
75
76
76
77 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
77 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
78 """ A Qt frontend for a generic Python kernel.
78 """ A Qt frontend for a generic Python kernel.
79 """
79 """
80
80
81 # The text to show when the kernel is (re)started.
81 # The text to show when the kernel is (re)started.
82 banner = Unicode(config=True)
82 banner = Unicode(config=True)
83
83
84 # An option and corresponding signal for overriding the default kernel
84 # An option and corresponding signal for overriding the default kernel
85 # interrupt behavior.
85 # interrupt behavior.
86 custom_interrupt = Bool(False)
86 custom_interrupt = Bool(False)
87 custom_interrupt_requested = QtCore.Signal()
87 custom_interrupt_requested = QtCore.Signal()
88
88
89 # An option and corresponding signals for overriding the default kernel
89 # An option and corresponding signals for overriding the default kernel
90 # restart behavior.
90 # restart behavior.
91 custom_restart = Bool(False)
91 custom_restart = Bool(False)
92 custom_restart_kernel_died = QtCore.Signal(float)
92 custom_restart_kernel_died = QtCore.Signal(float)
93 custom_restart_requested = QtCore.Signal()
93 custom_restart_requested = QtCore.Signal()
94
94
95 # Whether to automatically show calltips on open-parentheses.
95 # Whether to automatically show calltips on open-parentheses.
96 enable_calltips = Bool(True, config=True,
96 enable_calltips = Bool(True, config=True,
97 help="Whether to draw information calltips on open-parentheses.")
97 help="Whether to draw information calltips on open-parentheses.")
98
98
99 clear_on_kernel_restart = Bool(True, config=True,
99 clear_on_kernel_restart = Bool(True, config=True,
100 help="Whether to clear the console when the kernel is restarted")
100 help="Whether to clear the console when the kernel is restarted")
101
101
102 confirm_restart = Bool(True, config=True,
102 confirm_restart = Bool(True, config=True,
103 help="Whether to ask for user confirmation when restarting kernel")
103 help="Whether to ask for user confirmation when restarting kernel")
104
104
105 lexer_class = DottedObjectName(config=True,
105 lexer_class = DottedObjectName(config=True,
106 help="The pygments lexer class to use."
106 help="The pygments lexer class to use."
107 )
107 )
108 def _lexer_class_changed(self, name, old, new):
108 def _lexer_class_changed(self, name, old, new):
109 lexer_class = import_item(new)
109 lexer_class = import_item(new)
110 self.lexer = lexer_class()
110 self.lexer = lexer_class()
111
111
112 def _lexer_class_default(self):
112 def _lexer_class_default(self):
113 if py3compat.PY3:
113 if py3compat.PY3:
114 return 'pygments.lexers.Python3Lexer'
114 return 'pygments.lexers.Python3Lexer'
115 else:
115 else:
116 return 'pygments.lexers.PythonLexer'
116 return 'pygments.lexers.PythonLexer'
117
117
118 lexer = Any()
118 lexer = Any()
119 def _lexer_default(self):
119 def _lexer_default(self):
120 lexer_class = import_item(self.lexer_class)
120 lexer_class = import_item(self.lexer_class)
121 return lexer_class()
121 return lexer_class()
122
122
123 # Emitted when a user visible 'execute_request' has been submitted to the
123 # Emitted when a user visible 'execute_request' has been submitted to the
124 # kernel from the FrontendWidget. Contains the code to be executed.
124 # kernel from the FrontendWidget. Contains the code to be executed.
125 executing = QtCore.Signal(object)
125 executing = QtCore.Signal(object)
126
126
127 # Emitted when a user-visible 'execute_reply' has been received from the
127 # Emitted when a user-visible 'execute_reply' has been received from the
128 # kernel and processed by the FrontendWidget. Contains the response message.
128 # kernel and processed by the FrontendWidget. Contains the response message.
129 executed = QtCore.Signal(object)
129 executed = QtCore.Signal(object)
130
130
131 # Emitted when an exit request has been received from the kernel.
131 # Emitted when an exit request has been received from the kernel.
132 exit_requested = QtCore.Signal(object)
132 exit_requested = QtCore.Signal(object)
133
133
134 # Protected class variables.
134 # Protected class variables.
135 _prompt_transformer = IPythonInputSplitter(physical_line_transforms=[classic_prompt()],
135 _prompt_transformer = IPythonInputSplitter(physical_line_transforms=[classic_prompt()],
136 logical_line_transforms=[],
136 logical_line_transforms=[],
137 python_line_transforms=[],
137 python_line_transforms=[],
138 )
138 )
139 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
139 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
140 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
140 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
141 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
141 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
142 _input_splitter_class = InputSplitter
142 _input_splitter_class = InputSplitter
143 _local_kernel = False
143 _local_kernel = False
144 _highlighter = Instance(FrontendHighlighter)
144 _highlighter = Instance(FrontendHighlighter)
145
145
146 #---------------------------------------------------------------------------
146 #---------------------------------------------------------------------------
147 # 'object' interface
147 # 'object' interface
148 #---------------------------------------------------------------------------
148 #---------------------------------------------------------------------------
149
149
150 def __init__(self, *args, **kw):
150 def __init__(self, *args, **kw):
151 super(FrontendWidget, self).__init__(*args, **kw)
151 super(FrontendWidget, self).__init__(*args, **kw)
152 # FIXME: remove this when PySide min version is updated past 1.0.7
152 # FIXME: remove this when PySide min version is updated past 1.0.7
153 # forcefully disable calltips if PySide is < 1.0.7, because they crash
153 # forcefully disable calltips if PySide is < 1.0.7, because they crash
154 if qt.QT_API == qt.QT_API_PYSIDE:
154 if qt.QT_API == qt.QT_API_PYSIDE:
155 import PySide
155 import PySide
156 if PySide.__version_info__ < (1,0,7):
156 if PySide.__version_info__ < (1,0,7):
157 self.log.warn("PySide %s < 1.0.7 detected, disabling calltips" % PySide.__version__)
157 self.log.warn("PySide %s < 1.0.7 detected, disabling calltips" % PySide.__version__)
158 self.enable_calltips = False
158 self.enable_calltips = False
159
159
160 # FrontendWidget protected variables.
160 # FrontendWidget protected variables.
161 self._bracket_matcher = BracketMatcher(self._control)
161 self._bracket_matcher = BracketMatcher(self._control)
162 self._call_tip_widget = CallTipWidget(self._control)
162 self._call_tip_widget = CallTipWidget(self._control)
163 self._completion_lexer = CompletionLexer(self.lexer)
163 self._completion_lexer = CompletionLexer(self.lexer)
164 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
164 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
165 self._hidden = False
165 self._hidden = False
166 self._highlighter = FrontendHighlighter(self, lexer=self.lexer)
166 self._highlighter = FrontendHighlighter(self, lexer=self.lexer)
167 self._input_splitter = self._input_splitter_class()
167 self._input_splitter = self._input_splitter_class()
168 self._kernel_manager = None
168 self._kernel_manager = None
169 self._kernel_client = None
169 self._kernel_client = None
170 self._request_info = {}
170 self._request_info = {}
171 self._request_info['execute'] = {};
171 self._request_info['execute'] = {};
172 self._callback_dict = {}
172 self._callback_dict = {}
173
173
174 # Configure the ConsoleWidget.
174 # Configure the ConsoleWidget.
175 self.tab_width = 4
175 self.tab_width = 4
176 self._set_continuation_prompt('... ')
176 self._set_continuation_prompt('... ')
177
177
178 # Configure the CallTipWidget.
178 # Configure the CallTipWidget.
179 self._call_tip_widget.setFont(self.font)
179 self._call_tip_widget.setFont(self.font)
180 self.font_changed.connect(self._call_tip_widget.setFont)
180 self.font_changed.connect(self._call_tip_widget.setFont)
181
181
182 # Configure actions.
182 # Configure actions.
183 action = self._copy_raw_action
183 action = self._copy_raw_action
184 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
184 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
185 action.setEnabled(False)
185 action.setEnabled(False)
186 action.setShortcut(QtGui.QKeySequence(key))
186 action.setShortcut(QtGui.QKeySequence(key))
187 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
187 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
188 action.triggered.connect(self.copy_raw)
188 action.triggered.connect(self.copy_raw)
189 self.copy_available.connect(action.setEnabled)
189 self.copy_available.connect(action.setEnabled)
190 self.addAction(action)
190 self.addAction(action)
191
191
192 # Connect signal handlers.
192 # Connect signal handlers.
193 document = self._control.document()
193 document = self._control.document()
194 document.contentsChange.connect(self._document_contents_change)
194 document.contentsChange.connect(self._document_contents_change)
195
195
196 # Set flag for whether we are connected via localhost.
196 # Set flag for whether we are connected via localhost.
197 self._local_kernel = kw.get('local_kernel',
197 self._local_kernel = kw.get('local_kernel',
198 FrontendWidget._local_kernel)
198 FrontendWidget._local_kernel)
199
199
200 # Whether or not a clear_output call is pending new output.
201 self._pending_clearoutput = False
202
200 #---------------------------------------------------------------------------
203 #---------------------------------------------------------------------------
201 # 'ConsoleWidget' public interface
204 # 'ConsoleWidget' public interface
202 #---------------------------------------------------------------------------
205 #---------------------------------------------------------------------------
203
206
204 def copy(self):
207 def copy(self):
205 """ Copy the currently selected text to the clipboard, removing prompts.
208 """ Copy the currently selected text to the clipboard, removing prompts.
206 """
209 """
207 if self._page_control is not None and self._page_control.hasFocus():
210 if self._page_control is not None and self._page_control.hasFocus():
208 self._page_control.copy()
211 self._page_control.copy()
209 elif self._control.hasFocus():
212 elif self._control.hasFocus():
210 text = self._control.textCursor().selection().toPlainText()
213 text = self._control.textCursor().selection().toPlainText()
211 if text:
214 if text:
212 text = self._prompt_transformer.transform_cell(text)
215 text = self._prompt_transformer.transform_cell(text)
213 QtGui.QApplication.clipboard().setText(text)
216 QtGui.QApplication.clipboard().setText(text)
214 else:
217 else:
215 self.log.debug("frontend widget : unknown copy target")
218 self.log.debug("frontend widget : unknown copy target")
216
219
217 #---------------------------------------------------------------------------
220 #---------------------------------------------------------------------------
218 # 'ConsoleWidget' abstract interface
221 # 'ConsoleWidget' abstract interface
219 #---------------------------------------------------------------------------
222 #---------------------------------------------------------------------------
220
223
221 def _is_complete(self, source, interactive):
224 def _is_complete(self, source, interactive):
222 """ Returns whether 'source' can be completely processed and a new
225 """ Returns whether 'source' can be completely processed and a new
223 prompt created. When triggered by an Enter/Return key press,
226 prompt created. When triggered by an Enter/Return key press,
224 'interactive' is True; otherwise, it is False.
227 'interactive' is True; otherwise, it is False.
225 """
228 """
226 self._input_splitter.reset()
229 self._input_splitter.reset()
227 try:
230 try:
228 complete = self._input_splitter.push(source)
231 complete = self._input_splitter.push(source)
229 except SyntaxError:
232 except SyntaxError:
230 return True
233 return True
231 if interactive:
234 if interactive:
232 complete = not self._input_splitter.push_accepts_more()
235 complete = not self._input_splitter.push_accepts_more()
233 return complete
236 return complete
234
237
235 def _execute(self, source, hidden):
238 def _execute(self, source, hidden):
236 """ Execute 'source'. If 'hidden', do not show any output.
239 """ Execute 'source'. If 'hidden', do not show any output.
237
240
238 See parent class :meth:`execute` docstring for full details.
241 See parent class :meth:`execute` docstring for full details.
239 """
242 """
240 msg_id = self.kernel_client.execute(source, hidden)
243 msg_id = self.kernel_client.execute(source, hidden)
241 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'user')
244 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'user')
242 self._hidden = hidden
245 self._hidden = hidden
243 if not hidden:
246 if not hidden:
244 self.executing.emit(source)
247 self.executing.emit(source)
245
248
246 def _prompt_started_hook(self):
249 def _prompt_started_hook(self):
247 """ Called immediately after a new prompt is displayed.
250 """ Called immediately after a new prompt is displayed.
248 """
251 """
249 if not self._reading:
252 if not self._reading:
250 self._highlighter.highlighting_on = True
253 self._highlighter.highlighting_on = True
251
254
252 def _prompt_finished_hook(self):
255 def _prompt_finished_hook(self):
253 """ Called immediately after a prompt is finished, i.e. when some input
256 """ Called immediately after a prompt is finished, i.e. when some input
254 will be processed and a new prompt displayed.
257 will be processed and a new prompt displayed.
255 """
258 """
256 # Flush all state from the input splitter so the next round of
259 # Flush all state from the input splitter so the next round of
257 # reading input starts with a clean buffer.
260 # reading input starts with a clean buffer.
258 self._input_splitter.reset()
261 self._input_splitter.reset()
259
262
260 if not self._reading:
263 if not self._reading:
261 self._highlighter.highlighting_on = False
264 self._highlighter.highlighting_on = False
262
265
263 def _tab_pressed(self):
266 def _tab_pressed(self):
264 """ Called when the tab key is pressed. Returns whether to continue
267 """ Called when the tab key is pressed. Returns whether to continue
265 processing the event.
268 processing the event.
266 """
269 """
267 # Perform tab completion if:
270 # Perform tab completion if:
268 # 1) The cursor is in the input buffer.
271 # 1) The cursor is in the input buffer.
269 # 2) There is a non-whitespace character before the cursor.
272 # 2) There is a non-whitespace character before the cursor.
270 text = self._get_input_buffer_cursor_line()
273 text = self._get_input_buffer_cursor_line()
271 if text is None:
274 if text is None:
272 return False
275 return False
273 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
276 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
274 if complete:
277 if complete:
275 self._complete()
278 self._complete()
276 return not complete
279 return not complete
277
280
278 #---------------------------------------------------------------------------
281 #---------------------------------------------------------------------------
279 # 'ConsoleWidget' protected interface
282 # 'ConsoleWidget' protected interface
280 #---------------------------------------------------------------------------
283 #---------------------------------------------------------------------------
281
284
282 def _context_menu_make(self, pos):
285 def _context_menu_make(self, pos):
283 """ Reimplemented to add an action for raw copy.
286 """ Reimplemented to add an action for raw copy.
284 """
287 """
285 menu = super(FrontendWidget, self)._context_menu_make(pos)
288 menu = super(FrontendWidget, self)._context_menu_make(pos)
286 for before_action in menu.actions():
289 for before_action in menu.actions():
287 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
290 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
288 QtGui.QKeySequence.ExactMatch:
291 QtGui.QKeySequence.ExactMatch:
289 menu.insertAction(before_action, self._copy_raw_action)
292 menu.insertAction(before_action, self._copy_raw_action)
290 break
293 break
291 return menu
294 return menu
292
295
293 def request_interrupt_kernel(self):
296 def request_interrupt_kernel(self):
294 if self._executing:
297 if self._executing:
295 self.interrupt_kernel()
298 self.interrupt_kernel()
296
299
297 def request_restart_kernel(self):
300 def request_restart_kernel(self):
298 message = 'Are you sure you want to restart the kernel?'
301 message = 'Are you sure you want to restart the kernel?'
299 self.restart_kernel(message, now=False)
302 self.restart_kernel(message, now=False)
300
303
301 def _event_filter_console_keypress(self, event):
304 def _event_filter_console_keypress(self, event):
302 """ Reimplemented for execution interruption and smart backspace.
305 """ Reimplemented for execution interruption and smart backspace.
303 """
306 """
304 key = event.key()
307 key = event.key()
305 if self._control_key_down(event.modifiers(), include_command=False):
308 if self._control_key_down(event.modifiers(), include_command=False):
306
309
307 if key == QtCore.Qt.Key_C and self._executing:
310 if key == QtCore.Qt.Key_C and self._executing:
308 self.request_interrupt_kernel()
311 self.request_interrupt_kernel()
309 return True
312 return True
310
313
311 elif key == QtCore.Qt.Key_Period:
314 elif key == QtCore.Qt.Key_Period:
312 self.request_restart_kernel()
315 self.request_restart_kernel()
313 return True
316 return True
314
317
315 elif not event.modifiers() & QtCore.Qt.AltModifier:
318 elif not event.modifiers() & QtCore.Qt.AltModifier:
316
319
317 # Smart backspace: remove four characters in one backspace if:
320 # Smart backspace: remove four characters in one backspace if:
318 # 1) everything left of the cursor is whitespace
321 # 1) everything left of the cursor is whitespace
319 # 2) the four characters immediately left of the cursor are spaces
322 # 2) the four characters immediately left of the cursor are spaces
320 if key == QtCore.Qt.Key_Backspace:
323 if key == QtCore.Qt.Key_Backspace:
321 col = self._get_input_buffer_cursor_column()
324 col = self._get_input_buffer_cursor_column()
322 cursor = self._control.textCursor()
325 cursor = self._control.textCursor()
323 if col > 3 and not cursor.hasSelection():
326 if col > 3 and not cursor.hasSelection():
324 text = self._get_input_buffer_cursor_line()[:col]
327 text = self._get_input_buffer_cursor_line()[:col]
325 if text.endswith(' ') and not text.strip():
328 if text.endswith(' ') and not text.strip():
326 cursor.movePosition(QtGui.QTextCursor.Left,
329 cursor.movePosition(QtGui.QTextCursor.Left,
327 QtGui.QTextCursor.KeepAnchor, 4)
330 QtGui.QTextCursor.KeepAnchor, 4)
328 cursor.removeSelectedText()
331 cursor.removeSelectedText()
329 return True
332 return True
330
333
331 return super(FrontendWidget, self)._event_filter_console_keypress(event)
334 return super(FrontendWidget, self)._event_filter_console_keypress(event)
332
335
333 def _insert_continuation_prompt(self, cursor):
336 def _insert_continuation_prompt(self, cursor):
334 """ Reimplemented for auto-indentation.
337 """ Reimplemented for auto-indentation.
335 """
338 """
336 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
339 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
337 cursor.insertText(' ' * self._input_splitter.indent_spaces)
340 cursor.insertText(' ' * self._input_splitter.indent_spaces)
338
341
339 #---------------------------------------------------------------------------
342 #---------------------------------------------------------------------------
340 # 'BaseFrontendMixin' abstract interface
343 # 'BaseFrontendMixin' abstract interface
341 #---------------------------------------------------------------------------
344 #---------------------------------------------------------------------------
345 def _handle_clear_output(self, msg):
346 """Handle clear output messages."""
347 if not self._hidden and self._is_from_this_session(msg):
348 wait = msg['content'].get('wait', True)
349 if wait:
350 self._pending_clearoutput = True
351 else:
352 self.clear_output()
342
353
343 def _handle_complete_reply(self, rep):
354 def _handle_complete_reply(self, rep):
344 """ Handle replies for tab completion.
355 """ Handle replies for tab completion.
345 """
356 """
346 self.log.debug("complete: %s", rep.get('content', ''))
357 self.log.debug("complete: %s", rep.get('content', ''))
347 cursor = self._get_cursor()
358 cursor = self._get_cursor()
348 info = self._request_info.get('complete')
359 info = self._request_info.get('complete')
349 if info and info.id == rep['parent_header']['msg_id'] and \
360 if info and info.id == rep['parent_header']['msg_id'] and \
350 info.pos == cursor.position():
361 info.pos == cursor.position():
351 text = '.'.join(self._get_context())
362 text = '.'.join(self._get_context())
352 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
363 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
353 self._complete_with_items(cursor, rep['content']['matches'])
364 self._complete_with_items(cursor, rep['content']['matches'])
354
365
355 def _silent_exec_callback(self, expr, callback):
366 def _silent_exec_callback(self, expr, callback):
356 """Silently execute `expr` in the kernel and call `callback` with reply
367 """Silently execute `expr` in the kernel and call `callback` with reply
357
368
358 the `expr` is evaluated silently in the kernel (without) output in
369 the `expr` is evaluated silently in the kernel (without) output in
359 the frontend. Call `callback` with the
370 the frontend. Call `callback` with the
360 `repr <http://docs.python.org/library/functions.html#repr> `_ as first argument
371 `repr <http://docs.python.org/library/functions.html#repr> `_ as first argument
361
372
362 Parameters
373 Parameters
363 ----------
374 ----------
364 expr : string
375 expr : string
365 valid string to be executed by the kernel.
376 valid string to be executed by the kernel.
366 callback : function
377 callback : function
367 function accepting one argument, as a string. The string will be
378 function accepting one argument, as a string. The string will be
368 the `repr` of the result of evaluating `expr`
379 the `repr` of the result of evaluating `expr`
369
380
370 The `callback` is called with the `repr()` of the result of `expr` as
381 The `callback` is called with the `repr()` of the result of `expr` as
371 first argument. To get the object, do `eval()` on the passed value.
382 first argument. To get the object, do `eval()` on the passed value.
372
383
373 See Also
384 See Also
374 --------
385 --------
375 _handle_exec_callback : private method, deal with calling callback with reply
386 _handle_exec_callback : private method, deal with calling callback with reply
376
387
377 """
388 """
378
389
379 # generate uuid, which would be used as an indication of whether or
390 # generate uuid, which would be used as an indication of whether or
380 # not the unique request originated from here (can use msg id ?)
391 # not the unique request originated from here (can use msg id ?)
381 local_uuid = str(uuid.uuid1())
392 local_uuid = str(uuid.uuid1())
382 msg_id = self.kernel_client.execute('',
393 msg_id = self.kernel_client.execute('',
383 silent=True, user_expressions={ local_uuid:expr })
394 silent=True, user_expressions={ local_uuid:expr })
384 self._callback_dict[local_uuid] = callback
395 self._callback_dict[local_uuid] = callback
385 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'silent_exec_callback')
396 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'silent_exec_callback')
386
397
387 def _handle_exec_callback(self, msg):
398 def _handle_exec_callback(self, msg):
388 """Execute `callback` corresponding to `msg` reply, after ``_silent_exec_callback``
399 """Execute `callback` corresponding to `msg` reply, after ``_silent_exec_callback``
389
400
390 Parameters
401 Parameters
391 ----------
402 ----------
392 msg : raw message send by the kernel containing an `user_expressions`
403 msg : raw message send by the kernel containing an `user_expressions`
393 and having a 'silent_exec_callback' kind.
404 and having a 'silent_exec_callback' kind.
394
405
395 Notes
406 Notes
396 -----
407 -----
397 This function will look for a `callback` associated with the
408 This function will look for a `callback` associated with the
398 corresponding message id. Association has been made by
409 corresponding message id. Association has been made by
399 `_silent_exec_callback`. `callback` is then called with the `repr()`
410 `_silent_exec_callback`. `callback` is then called with the `repr()`
400 of the value of corresponding `user_expressions` as argument.
411 of the value of corresponding `user_expressions` as argument.
401 `callback` is then removed from the known list so that any message
412 `callback` is then removed from the known list so that any message
402 coming again with the same id won't trigger it.
413 coming again with the same id won't trigger it.
403
414
404 """
415 """
405
416
406 user_exp = msg['content'].get('user_expressions')
417 user_exp = msg['content'].get('user_expressions')
407 if not user_exp:
418 if not user_exp:
408 return
419 return
409 for expression in user_exp:
420 for expression in user_exp:
410 if expression in self._callback_dict:
421 if expression in self._callback_dict:
411 self._callback_dict.pop(expression)(user_exp[expression])
422 self._callback_dict.pop(expression)(user_exp[expression])
412
423
413 def _handle_execute_reply(self, msg):
424 def _handle_execute_reply(self, msg):
414 """ Handles replies for code execution.
425 """ Handles replies for code execution.
415 """
426 """
416 self.log.debug("execute: %s", msg.get('content', ''))
427 self.log.debug("execute: %s", msg.get('content', ''))
417 msg_id = msg['parent_header']['msg_id']
428 msg_id = msg['parent_header']['msg_id']
418 info = self._request_info['execute'].get(msg_id)
429 info = self._request_info['execute'].get(msg_id)
419 # unset reading flag, because if execute finished, raw_input can't
430 # unset reading flag, because if execute finished, raw_input can't
420 # still be pending.
431 # still be pending.
421 self._reading = False
432 self._reading = False
422 if info and info.kind == 'user' and not self._hidden:
433 if info and info.kind == 'user' and not self._hidden:
423 # Make sure that all output from the SUB channel has been processed
434 # Make sure that all output from the SUB channel has been processed
424 # before writing a new prompt.
435 # before writing a new prompt.
425 self.kernel_client.iopub_channel.flush()
436 self.kernel_client.iopub_channel.flush()
426
437
427 # Reset the ANSI style information to prevent bad text in stdout
438 # Reset the ANSI style information to prevent bad text in stdout
428 # from messing up our colors. We're not a true terminal so we're
439 # from messing up our colors. We're not a true terminal so we're
429 # allowed to do this.
440 # allowed to do this.
430 if self.ansi_codes:
441 if self.ansi_codes:
431 self._ansi_processor.reset_sgr()
442 self._ansi_processor.reset_sgr()
432
443
433 content = msg['content']
444 content = msg['content']
434 status = content['status']
445 status = content['status']
435 if status == 'ok':
446 if status == 'ok':
436 self._process_execute_ok(msg)
447 self._process_execute_ok(msg)
437 elif status == 'error':
448 elif status == 'error':
438 self._process_execute_error(msg)
449 self._process_execute_error(msg)
439 elif status == 'aborted':
450 elif status == 'aborted':
440 self._process_execute_abort(msg)
451 self._process_execute_abort(msg)
441
452
442 self._show_interpreter_prompt_for_reply(msg)
453 self._show_interpreter_prompt_for_reply(msg)
443 self.executed.emit(msg)
454 self.executed.emit(msg)
444 self._request_info['execute'].pop(msg_id)
455 self._request_info['execute'].pop(msg_id)
445 elif info and info.kind == 'silent_exec_callback' and not self._hidden:
456 elif info and info.kind == 'silent_exec_callback' and not self._hidden:
446 self._handle_exec_callback(msg)
457 self._handle_exec_callback(msg)
447 self._request_info['execute'].pop(msg_id)
458 self._request_info['execute'].pop(msg_id)
448 else:
459 else:
449 super(FrontendWidget, self)._handle_execute_reply(msg)
460 super(FrontendWidget, self)._handle_execute_reply(msg)
450
461
451 def _handle_input_request(self, msg):
462 def _handle_input_request(self, msg):
452 """ Handle requests for raw_input.
463 """ Handle requests for raw_input.
453 """
464 """
454 self.log.debug("input: %s", msg.get('content', ''))
465 self.log.debug("input: %s", msg.get('content', ''))
455 if self._hidden:
466 if self._hidden:
456 raise RuntimeError('Request for raw input during hidden execution.')
467 raise RuntimeError('Request for raw input during hidden execution.')
457
468
458 # Make sure that all output from the SUB channel has been processed
469 # Make sure that all output from the SUB channel has been processed
459 # before entering readline mode.
470 # before entering readline mode.
460 self.kernel_client.iopub_channel.flush()
471 self.kernel_client.iopub_channel.flush()
461
472
462 def callback(line):
473 def callback(line):
463 self.kernel_client.stdin_channel.input(line)
474 self.kernel_client.stdin_channel.input(line)
464 if self._reading:
475 if self._reading:
465 self.log.debug("Got second input request, assuming first was interrupted.")
476 self.log.debug("Got second input request, assuming first was interrupted.")
466 self._reading = False
477 self._reading = False
467 self._readline(msg['content']['prompt'], callback=callback)
478 self._readline(msg['content']['prompt'], callback=callback)
468
479
469 def _kernel_restarted_message(self, died=True):
480 def _kernel_restarted_message(self, died=True):
470 msg = "Kernel died, restarting" if died else "Kernel restarting"
481 msg = "Kernel died, restarting" if died else "Kernel restarting"
471 self._append_html("<br>%s<hr><br>" % msg,
482 self._append_html("<br>%s<hr><br>" % msg,
472 before_prompt=False
483 before_prompt=False
473 )
484 )
474
485
475 def _handle_kernel_died(self, since_last_heartbeat):
486 def _handle_kernel_died(self, since_last_heartbeat):
476 """Handle the kernel's death (if we do not own the kernel).
487 """Handle the kernel's death (if we do not own the kernel).
477 """
488 """
478 self.log.warn("kernel died: %s", since_last_heartbeat)
489 self.log.warn("kernel died: %s", since_last_heartbeat)
479 if self.custom_restart:
490 if self.custom_restart:
480 self.custom_restart_kernel_died.emit(since_last_heartbeat)
491 self.custom_restart_kernel_died.emit(since_last_heartbeat)
481 else:
492 else:
482 self._kernel_restarted_message(died=True)
493 self._kernel_restarted_message(died=True)
483 self.reset()
494 self.reset()
484
495
485 def _handle_kernel_restarted(self, died=True):
496 def _handle_kernel_restarted(self, died=True):
486 """Notice that the autorestarter restarted the kernel.
497 """Notice that the autorestarter restarted the kernel.
487
498
488 There's nothing to do but show a message.
499 There's nothing to do but show a message.
489 """
500 """
490 self.log.warn("kernel restarted")
501 self.log.warn("kernel restarted")
491 self._kernel_restarted_message(died=died)
502 self._kernel_restarted_message(died=died)
492 self.reset()
503 self.reset()
493
504
494 def _handle_object_info_reply(self, rep):
505 def _handle_object_info_reply(self, rep):
495 """ Handle replies for call tips.
506 """ Handle replies for call tips.
496 """
507 """
497 self.log.debug("oinfo: %s", rep.get('content', ''))
508 self.log.debug("oinfo: %s", rep.get('content', ''))
498 cursor = self._get_cursor()
509 cursor = self._get_cursor()
499 info = self._request_info.get('call_tip')
510 info = self._request_info.get('call_tip')
500 if info and info.id == rep['parent_header']['msg_id'] and \
511 if info and info.id == rep['parent_header']['msg_id'] and \
501 info.pos == cursor.position():
512 info.pos == cursor.position():
502 # Get the information for a call tip. For now we format the call
513 # Get the information for a call tip. For now we format the call
503 # line as string, later we can pass False to format_call and
514 # line as string, later we can pass False to format_call and
504 # syntax-highlight it ourselves for nicer formatting in the
515 # syntax-highlight it ourselves for nicer formatting in the
505 # calltip.
516 # calltip.
506 content = rep['content']
517 content = rep['content']
507 # if this is from pykernel, 'docstring' will be the only key
518 # if this is from pykernel, 'docstring' will be the only key
508 if content.get('ismagic', False):
519 if content.get('ismagic', False):
509 # Don't generate a call-tip for magics. Ideally, we should
520 # Don't generate a call-tip for magics. Ideally, we should
510 # generate a tooltip, but not on ( like we do for actual
521 # generate a tooltip, but not on ( like we do for actual
511 # callables.
522 # callables.
512 call_info, doc = None, None
523 call_info, doc = None, None
513 else:
524 else:
514 call_info, doc = call_tip(content, format_call=True)
525 call_info, doc = call_tip(content, format_call=True)
515 if call_info or doc:
526 if call_info or doc:
516 self._call_tip_widget.show_call_info(call_info, doc)
527 self._call_tip_widget.show_call_info(call_info, doc)
517
528
518 def _handle_pyout(self, msg):
529 def _handle_pyout(self, msg):
519 """ Handle display hook output.
530 """ Handle display hook output.
520 """
531 """
521 self.log.debug("pyout: %s", msg.get('content', ''))
532 self.log.debug("pyout: %s", msg.get('content', ''))
522 if not self._hidden and self._is_from_this_session(msg):
533 if not self._hidden and self._is_from_this_session(msg):
534 self.flush_clearoutput()
523 text = msg['content']['data']
535 text = msg['content']['data']
524 self._append_plain_text(text + '\n', before_prompt=True)
536 self._append_plain_text(text + '\n', before_prompt=True)
525
537
526 def _handle_stream(self, msg):
538 def _handle_stream(self, msg):
527 """ Handle stdout, stderr, and stdin.
539 """ Handle stdout, stderr, and stdin.
528 """
540 """
529 self.log.debug("stream: %s", msg.get('content', ''))
541 self.log.debug("stream: %s", msg.get('content', ''))
530 if not self._hidden and self._is_from_this_session(msg):
542 if not self._hidden and self._is_from_this_session(msg):
531 # Most consoles treat tabs as being 8 space characters. Convert tabs
543 self.flush_clearoutput()
532 # to spaces so that output looks as expected regardless of this
544 self.append_stream(msg['content']['data'])
533 # widget's tab width.
534 text = msg['content']['data'].expandtabs(8)
535
536 self._append_plain_text(text, before_prompt=True)
537 self._control.moveCursor(QtGui.QTextCursor.End)
538
545
539 def _handle_shutdown_reply(self, msg):
546 def _handle_shutdown_reply(self, msg):
540 """ Handle shutdown signal, only if from other console.
547 """ Handle shutdown signal, only if from other console.
541 """
548 """
542 self.log.warn("shutdown: %s", msg.get('content', ''))
549 self.log.warn("shutdown: %s", msg.get('content', ''))
543 restart = msg.get('content', {}).get('restart', False)
550 restart = msg.get('content', {}).get('restart', False)
544 if not self._hidden and not self._is_from_this_session(msg):
551 if not self._hidden and not self._is_from_this_session(msg):
545 # got shutdown reply, request came from session other than ours
552 # got shutdown reply, request came from session other than ours
546 if restart:
553 if restart:
547 # someone restarted the kernel, handle it
554 # someone restarted the kernel, handle it
548 self._handle_kernel_restarted(died=False)
555 self._handle_kernel_restarted(died=False)
549 else:
556 else:
550 # kernel was shutdown permanently
557 # kernel was shutdown permanently
551 # this triggers exit_requested if the kernel was local,
558 # this triggers exit_requested if the kernel was local,
552 # and a dialog if the kernel was remote,
559 # and a dialog if the kernel was remote,
553 # so we don't suddenly clear the qtconsole without asking.
560 # so we don't suddenly clear the qtconsole without asking.
554 if self._local_kernel:
561 if self._local_kernel:
555 self.exit_requested.emit(self)
562 self.exit_requested.emit(self)
556 else:
563 else:
557 title = self.window().windowTitle()
564 title = self.window().windowTitle()
558 reply = QtGui.QMessageBox.question(self, title,
565 reply = QtGui.QMessageBox.question(self, title,
559 "Kernel has been shutdown permanently. "
566 "Kernel has been shutdown permanently. "
560 "Close the Console?",
567 "Close the Console?",
561 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
568 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
562 if reply == QtGui.QMessageBox.Yes:
569 if reply == QtGui.QMessageBox.Yes:
563 self.exit_requested.emit(self)
570 self.exit_requested.emit(self)
564
571
565 def _handle_status(self, msg):
572 def _handle_status(self, msg):
566 """Handle status message"""
573 """Handle status message"""
567 # This is where a busy/idle indicator would be triggered,
574 # This is where a busy/idle indicator would be triggered,
568 # when we make one.
575 # when we make one.
569 state = msg['content'].get('execution_state', '')
576 state = msg['content'].get('execution_state', '')
570 if state == 'starting':
577 if state == 'starting':
571 # kernel started while we were running
578 # kernel started while we were running
572 if self._executing:
579 if self._executing:
573 self._handle_kernel_restarted(died=True)
580 self._handle_kernel_restarted(died=True)
574 elif state == 'idle':
581 elif state == 'idle':
575 pass
582 pass
576 elif state == 'busy':
583 elif state == 'busy':
577 pass
584 pass
578
585
579 def _started_channels(self):
586 def _started_channels(self):
580 """ Called when the KernelManager channels have started listening or
587 """ Called when the KernelManager channels have started listening or
581 when the frontend is assigned an already listening KernelManager.
588 when the frontend is assigned an already listening KernelManager.
582 """
589 """
583 self.reset(clear=True)
590 self.reset(clear=True)
584
591
585 #---------------------------------------------------------------------------
592 #---------------------------------------------------------------------------
586 # 'FrontendWidget' public interface
593 # 'FrontendWidget' public interface
587 #---------------------------------------------------------------------------
594 #---------------------------------------------------------------------------
588
595
589 def copy_raw(self):
596 def copy_raw(self):
590 """ Copy the currently selected text to the clipboard without attempting
597 """ Copy the currently selected text to the clipboard without attempting
591 to remove prompts or otherwise alter the text.
598 to remove prompts or otherwise alter the text.
592 """
599 """
593 self._control.copy()
600 self._control.copy()
594
601
595 def execute_file(self, path, hidden=False):
602 def execute_file(self, path, hidden=False):
596 """ Attempts to execute file with 'path'. If 'hidden', no output is
603 """ Attempts to execute file with 'path'. If 'hidden', no output is
597 shown.
604 shown.
598 """
605 """
599 self.execute('execfile(%r)' % path, hidden=hidden)
606 self.execute('execfile(%r)' % path, hidden=hidden)
600
607
601 def interrupt_kernel(self):
608 def interrupt_kernel(self):
602 """ Attempts to interrupt the running kernel.
609 """ Attempts to interrupt the running kernel.
603
610
604 Also unsets _reading flag, to avoid runtime errors
611 Also unsets _reading flag, to avoid runtime errors
605 if raw_input is called again.
612 if raw_input is called again.
606 """
613 """
607 if self.custom_interrupt:
614 if self.custom_interrupt:
608 self._reading = False
615 self._reading = False
609 self.custom_interrupt_requested.emit()
616 self.custom_interrupt_requested.emit()
610 elif self.kernel_manager:
617 elif self.kernel_manager:
611 self._reading = False
618 self._reading = False
612 self.kernel_manager.interrupt_kernel()
619 self.kernel_manager.interrupt_kernel()
613 else:
620 else:
614 self._append_plain_text('Cannot interrupt a kernel I did not start.\n')
621 self._append_plain_text('Cannot interrupt a kernel I did not start.\n')
615
622
616 def reset(self, clear=False):
623 def reset(self, clear=False):
617 """ Resets the widget to its initial state if ``clear`` parameter
624 """ Resets the widget to its initial state if ``clear`` parameter
618 is True, otherwise
625 is True, otherwise
619 prints a visual indication of the fact that the kernel restarted, but
626 prints a visual indication of the fact that the kernel restarted, but
620 does not clear the traces from previous usage of the kernel before it
627 does not clear the traces from previous usage of the kernel before it
621 was restarted. With ``clear=True``, it is similar to ``%clear``, but
628 was restarted. With ``clear=True``, it is similar to ``%clear``, but
622 also re-writes the banner and aborts execution if necessary.
629 also re-writes the banner and aborts execution if necessary.
623 """
630 """
624 if self._executing:
631 if self._executing:
625 self._executing = False
632 self._executing = False
626 self._request_info['execute'] = {}
633 self._request_info['execute'] = {}
627 self._reading = False
634 self._reading = False
628 self._highlighter.highlighting_on = False
635 self._highlighter.highlighting_on = False
629
636
630 if clear:
637 if clear:
631 self._control.clear()
638 self._control.clear()
632 self._append_plain_text(self.banner)
639 self._append_plain_text(self.banner)
633 # update output marker for stdout/stderr, so that startup
640 # update output marker for stdout/stderr, so that startup
634 # messages appear after banner:
641 # messages appear after banner:
635 self._append_before_prompt_pos = self._get_cursor().position()
642 self._append_before_prompt_pos = self._get_cursor().position()
636 self._show_interpreter_prompt()
643 self._show_interpreter_prompt()
637
644
638 def restart_kernel(self, message, now=False):
645 def restart_kernel(self, message, now=False):
639 """ Attempts to restart the running kernel.
646 """ Attempts to restart the running kernel.
640 """
647 """
641 # FIXME: now should be configurable via a checkbox in the dialog. Right
648 # FIXME: now should be configurable via a checkbox in the dialog. Right
642 # now at least the heartbeat path sets it to True and the manual restart
649 # now at least the heartbeat path sets it to True and the manual restart
643 # to False. But those should just be the pre-selected states of a
650 # to False. But those should just be the pre-selected states of a
644 # checkbox that the user could override if so desired. But I don't know
651 # checkbox that the user could override if so desired. But I don't know
645 # enough Qt to go implementing the checkbox now.
652 # enough Qt to go implementing the checkbox now.
646
653
647 if self.custom_restart:
654 if self.custom_restart:
648 self.custom_restart_requested.emit()
655 self.custom_restart_requested.emit()
649 return
656 return
650
657
651 if self.kernel_manager:
658 if self.kernel_manager:
652 # Pause the heart beat channel to prevent further warnings.
659 # Pause the heart beat channel to prevent further warnings.
653 self.kernel_client.hb_channel.pause()
660 self.kernel_client.hb_channel.pause()
654
661
655 # Prompt the user to restart the kernel. Un-pause the heartbeat if
662 # Prompt the user to restart the kernel. Un-pause the heartbeat if
656 # they decline. (If they accept, the heartbeat will be un-paused
663 # they decline. (If they accept, the heartbeat will be un-paused
657 # automatically when the kernel is restarted.)
664 # automatically when the kernel is restarted.)
658 if self.confirm_restart:
665 if self.confirm_restart:
659 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
666 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
660 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
667 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
661 message, buttons)
668 message, buttons)
662 do_restart = result == QtGui.QMessageBox.Yes
669 do_restart = result == QtGui.QMessageBox.Yes
663 else:
670 else:
664 # confirm_restart is False, so we don't need to ask user
671 # confirm_restart is False, so we don't need to ask user
665 # anything, just do the restart
672 # anything, just do the restart
666 do_restart = True
673 do_restart = True
667 if do_restart:
674 if do_restart:
668 try:
675 try:
669 self.kernel_manager.restart_kernel(now=now)
676 self.kernel_manager.restart_kernel(now=now)
670 except RuntimeError as e:
677 except RuntimeError as e:
671 self._append_plain_text(
678 self._append_plain_text(
672 'Error restarting kernel: %s\n' % e,
679 'Error restarting kernel: %s\n' % e,
673 before_prompt=True
680 before_prompt=True
674 )
681 )
675 else:
682 else:
676 self._append_html("<br>Restarting kernel...\n<hr><br>",
683 self._append_html("<br>Restarting kernel...\n<hr><br>",
677 before_prompt=True,
684 before_prompt=True,
678 )
685 )
679 else:
686 else:
680 self.kernel_client.hb_channel.unpause()
687 self.kernel_client.hb_channel.unpause()
681
688
682 else:
689 else:
683 self._append_plain_text(
690 self._append_plain_text(
684 'Cannot restart a Kernel I did not start\n',
691 'Cannot restart a Kernel I did not start\n',
685 before_prompt=True
692 before_prompt=True
686 )
693 )
687
694
695 def append_stream(self, text):
696 """Appends text to the output stream."""
697 # Most consoles treat tabs as being 8 space characters. Convert tabs
698 # to spaces so that output looks as expected regardless of this
699 # widget's tab width.
700 text = text.expandtabs(8)
701
702 print([ord(c) for c in text])
703 self._append_plain_text(text, before_prompt=True)
704 self._control.moveCursor(QtGui.QTextCursor.End)
705
706 def flush_clearoutput(self):
707 """If a clearoutput is pending, execute it."""
708 if self._pending_clearoutput:
709 self._pending_clearoutput = False
710 self.clear_output()
711
712 def clear_output(self):
713 """Clear the output area."""
714 cursor = self._control.textCursor()
715 cursor.beginEditBlock()
716 cursor.movePosition(cursor.StartOfLine, cursor.KeepAnchor)
717 cursor.insertText('')
718 cursor.endEditBlock()
719
688 #---------------------------------------------------------------------------
720 #---------------------------------------------------------------------------
689 # 'FrontendWidget' protected interface
721 # 'FrontendWidget' protected interface
690 #---------------------------------------------------------------------------
722 #---------------------------------------------------------------------------
691
723
692 def _call_tip(self):
724 def _call_tip(self):
693 """ Shows a call tip, if appropriate, at the current cursor location.
725 """ Shows a call tip, if appropriate, at the current cursor location.
694 """
726 """
695 # Decide if it makes sense to show a call tip
727 # Decide if it makes sense to show a call tip
696 if not self.enable_calltips:
728 if not self.enable_calltips:
697 return False
729 return False
698 cursor = self._get_cursor()
730 cursor = self._get_cursor()
699 cursor.movePosition(QtGui.QTextCursor.Left)
731 cursor.movePosition(QtGui.QTextCursor.Left)
700 if cursor.document().characterAt(cursor.position()) != '(':
732 if cursor.document().characterAt(cursor.position()) != '(':
701 return False
733 return False
702 context = self._get_context(cursor)
734 context = self._get_context(cursor)
703 if not context:
735 if not context:
704 return False
736 return False
705
737
706 # Send the metadata request to the kernel
738 # Send the metadata request to the kernel
707 name = '.'.join(context)
739 name = '.'.join(context)
708 msg_id = self.kernel_client.object_info(name)
740 msg_id = self.kernel_client.object_info(name)
709 pos = self._get_cursor().position()
741 pos = self._get_cursor().position()
710 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
742 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
711 return True
743 return True
712
744
713 def _complete(self):
745 def _complete(self):
714 """ Performs completion at the current cursor location.
746 """ Performs completion at the current cursor location.
715 """
747 """
716 context = self._get_context()
748 context = self._get_context()
717 if context:
749 if context:
718 # Send the completion request to the kernel
750 # Send the completion request to the kernel
719 msg_id = self.kernel_client.complete(
751 msg_id = self.kernel_client.complete(
720 '.'.join(context), # text
752 '.'.join(context), # text
721 self._get_input_buffer_cursor_line(), # line
753 self._get_input_buffer_cursor_line(), # line
722 self._get_input_buffer_cursor_column(), # cursor_pos
754 self._get_input_buffer_cursor_column(), # cursor_pos
723 self.input_buffer) # block
755 self.input_buffer) # block
724 pos = self._get_cursor().position()
756 pos = self._get_cursor().position()
725 info = self._CompletionRequest(msg_id, pos)
757 info = self._CompletionRequest(msg_id, pos)
726 self._request_info['complete'] = info
758 self._request_info['complete'] = info
727
759
728 def _get_context(self, cursor=None):
760 def _get_context(self, cursor=None):
729 """ Gets the context for the specified cursor (or the current cursor
761 """ Gets the context for the specified cursor (or the current cursor
730 if none is specified).
762 if none is specified).
731 """
763 """
732 if cursor is None:
764 if cursor is None:
733 cursor = self._get_cursor()
765 cursor = self._get_cursor()
734 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
766 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
735 QtGui.QTextCursor.KeepAnchor)
767 QtGui.QTextCursor.KeepAnchor)
736 text = cursor.selection().toPlainText()
768 text = cursor.selection().toPlainText()
737 return self._completion_lexer.get_context(text)
769 return self._completion_lexer.get_context(text)
738
770
739 def _process_execute_abort(self, msg):
771 def _process_execute_abort(self, msg):
740 """ Process a reply for an aborted execution request.
772 """ Process a reply for an aborted execution request.
741 """
773 """
742 self._append_plain_text("ERROR: execution aborted\n")
774 self._append_plain_text("ERROR: execution aborted\n")
743
775
744 def _process_execute_error(self, msg):
776 def _process_execute_error(self, msg):
745 """ Process a reply for an execution request that resulted in an error.
777 """ Process a reply for an execution request that resulted in an error.
746 """
778 """
747 content = msg['content']
779 content = msg['content']
748 # If a SystemExit is passed along, this means exit() was called - also
780 # If a SystemExit is passed along, this means exit() was called - also
749 # all the ipython %exit magic syntax of '-k' to be used to keep
781 # all the ipython %exit magic syntax of '-k' to be used to keep
750 # the kernel running
782 # the kernel running
751 if content['ename']=='SystemExit':
783 if content['ename']=='SystemExit':
752 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
784 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
753 self._keep_kernel_on_exit = keepkernel
785 self._keep_kernel_on_exit = keepkernel
754 self.exit_requested.emit(self)
786 self.exit_requested.emit(self)
755 else:
787 else:
756 traceback = ''.join(content['traceback'])
788 traceback = ''.join(content['traceback'])
757 self._append_plain_text(traceback)
789 self._append_plain_text(traceback)
758
790
759 def _process_execute_ok(self, msg):
791 def _process_execute_ok(self, msg):
760 """ Process a reply for a successful execution request.
792 """ Process a reply for a successful execution request.
761 """
793 """
762 payload = msg['content']['payload']
794 payload = msg['content']['payload']
763 for item in payload:
795 for item in payload:
764 if not self._process_execute_payload(item):
796 if not self._process_execute_payload(item):
765 warning = 'Warning: received unknown payload of type %s'
797 warning = 'Warning: received unknown payload of type %s'
766 print(warning % repr(item['source']))
798 print(warning % repr(item['source']))
767
799
768 def _process_execute_payload(self, item):
800 def _process_execute_payload(self, item):
769 """ Process a single payload item from the list of payload items in an
801 """ Process a single payload item from the list of payload items in an
770 execution reply. Returns whether the payload was handled.
802 execution reply. Returns whether the payload was handled.
771 """
803 """
772 # The basic FrontendWidget doesn't handle payloads, as they are a
804 # The basic FrontendWidget doesn't handle payloads, as they are a
773 # mechanism for going beyond the standard Python interpreter model.
805 # mechanism for going beyond the standard Python interpreter model.
774 return False
806 return False
775
807
776 def _show_interpreter_prompt(self):
808 def _show_interpreter_prompt(self):
777 """ Shows a prompt for the interpreter.
809 """ Shows a prompt for the interpreter.
778 """
810 """
779 self._show_prompt('>>> ')
811 self._show_prompt('>>> ')
780
812
781 def _show_interpreter_prompt_for_reply(self, msg):
813 def _show_interpreter_prompt_for_reply(self, msg):
782 """ Shows a prompt for the interpreter given an 'execute_reply' message.
814 """ Shows a prompt for the interpreter given an 'execute_reply' message.
783 """
815 """
784 self._show_interpreter_prompt()
816 self._show_interpreter_prompt()
785
817
786 #------ Signal handlers ----------------------------------------------------
818 #------ Signal handlers ----------------------------------------------------
787
819
788 def _document_contents_change(self, position, removed, added):
820 def _document_contents_change(self, position, removed, added):
789 """ Called whenever the document's content changes. Display a call tip
821 """ Called whenever the document's content changes. Display a call tip
790 if appropriate.
822 if appropriate.
791 """
823 """
792 # Calculate where the cursor should be *after* the change:
824 # Calculate where the cursor should be *after* the change:
793 position += added
825 position += added
794
826
795 document = self._control.document()
827 document = self._control.document()
796 if position == self._get_cursor().position():
828 if position == self._get_cursor().position():
797 self._call_tip()
829 self._call_tip()
798
830
799 #------ Trait default initializers -----------------------------------------
831 #------ Trait default initializers -----------------------------------------
800
832
801 def _banner_default(self):
833 def _banner_default(self):
802 """ Returns the standard Python banner.
834 """ Returns the standard Python banner.
803 """
835 """
804 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
836 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
805 '"license" for more information.'
837 '"license" for more information.'
806 return banner % (sys.version, sys.platform)
838 return banner % (sys.version, sys.platform)
@@ -1,602 +1,603 b''
1 """A FrontendWidget that emulates the interface of the console IPython.
1 """A FrontendWidget that emulates the interface of the console IPython.
2
2
3 This supports the additional functionality provided by the IPython kernel.
3 This supports the additional functionality provided by the IPython kernel.
4 """
4 """
5
5
6 #-----------------------------------------------------------------------------
6 #-----------------------------------------------------------------------------
7 # Imports
7 # Imports
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9
9
10 # Standard library imports
10 # Standard library imports
11 from collections import namedtuple
11 from collections import namedtuple
12 import os.path
12 import os.path
13 import re
13 import re
14 from subprocess import Popen
14 from subprocess import Popen
15 import sys
15 import sys
16 import time
16 import time
17 from textwrap import dedent
17 from textwrap import dedent
18
18
19 # System library imports
19 # System library imports
20 from IPython.external.qt import QtCore, QtGui
20 from IPython.external.qt import QtCore, QtGui
21
21
22 # Local imports
22 # Local imports
23 from IPython.core.inputsplitter import IPythonInputSplitter
23 from IPython.core.inputsplitter import IPythonInputSplitter
24 from IPython.core.inputtransformer import ipy_prompt
24 from IPython.core.inputtransformer import ipy_prompt
25 from IPython.utils.traitlets import Bool, Unicode
25 from IPython.utils.traitlets import Bool, Unicode
26 from .frontend_widget import FrontendWidget
26 from .frontend_widget import FrontendWidget
27 from . import styles
27 from . import styles
28
28
29 #-----------------------------------------------------------------------------
29 #-----------------------------------------------------------------------------
30 # Constants
30 # Constants
31 #-----------------------------------------------------------------------------
31 #-----------------------------------------------------------------------------
32
32
33 # Default strings to build and display input and output prompts (and separators
33 # Default strings to build and display input and output prompts (and separators
34 # in between)
34 # in between)
35 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
35 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
36 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
36 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
37 default_input_sep = '\n'
37 default_input_sep = '\n'
38 default_output_sep = ''
38 default_output_sep = ''
39 default_output_sep2 = ''
39 default_output_sep2 = ''
40
40
41 # Base path for most payload sources.
41 # Base path for most payload sources.
42 zmq_shell_source = 'IPython.kernel.zmq.zmqshell.ZMQInteractiveShell'
42 zmq_shell_source = 'IPython.kernel.zmq.zmqshell.ZMQInteractiveShell'
43
43
44 if sys.platform.startswith('win'):
44 if sys.platform.startswith('win'):
45 default_editor = 'notepad'
45 default_editor = 'notepad'
46 else:
46 else:
47 default_editor = ''
47 default_editor = ''
48
48
49 #-----------------------------------------------------------------------------
49 #-----------------------------------------------------------------------------
50 # IPythonWidget class
50 # IPythonWidget class
51 #-----------------------------------------------------------------------------
51 #-----------------------------------------------------------------------------
52
52
53 class IPythonWidget(FrontendWidget):
53 class IPythonWidget(FrontendWidget):
54 """ A FrontendWidget for an IPython kernel.
54 """ A FrontendWidget for an IPython kernel.
55 """
55 """
56
56
57 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
57 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
58 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
58 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
59 # settings.
59 # settings.
60 custom_edit = Bool(False)
60 custom_edit = Bool(False)
61 custom_edit_requested = QtCore.Signal(object, object)
61 custom_edit_requested = QtCore.Signal(object, object)
62
62
63 editor = Unicode(default_editor, config=True,
63 editor = Unicode(default_editor, config=True,
64 help="""
64 help="""
65 A command for invoking a system text editor. If the string contains a
65 A command for invoking a system text editor. If the string contains a
66 {filename} format specifier, it will be used. Otherwise, the filename
66 {filename} format specifier, it will be used. Otherwise, the filename
67 will be appended to the end the command.
67 will be appended to the end the command.
68 """)
68 """)
69
69
70 editor_line = Unicode(config=True,
70 editor_line = Unicode(config=True,
71 help="""
71 help="""
72 The editor command to use when a specific line number is requested. The
72 The editor command to use when a specific line number is requested. The
73 string should contain two format specifiers: {line} and {filename}. If
73 string should contain two format specifiers: {line} and {filename}. If
74 this parameter is not specified, the line number option to the %edit
74 this parameter is not specified, the line number option to the %edit
75 magic will be ignored.
75 magic will be ignored.
76 """)
76 """)
77
77
78 style_sheet = Unicode(config=True,
78 style_sheet = Unicode(config=True,
79 help="""
79 help="""
80 A CSS stylesheet. The stylesheet can contain classes for:
80 A CSS stylesheet. The stylesheet can contain classes for:
81 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
81 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
82 2. Pygments: .c, .k, .o, etc. (see PygmentsHighlighter)
82 2. Pygments: .c, .k, .o, etc. (see PygmentsHighlighter)
83 3. IPython: .error, .in-prompt, .out-prompt, etc
83 3. IPython: .error, .in-prompt, .out-prompt, etc
84 """)
84 """)
85
85
86 syntax_style = Unicode(config=True,
86 syntax_style = Unicode(config=True,
87 help="""
87 help="""
88 If not empty, use this Pygments style for syntax highlighting.
88 If not empty, use this Pygments style for syntax highlighting.
89 Otherwise, the style sheet is queried for Pygments style
89 Otherwise, the style sheet is queried for Pygments style
90 information.
90 information.
91 """)
91 """)
92
92
93 # Prompts.
93 # Prompts.
94 in_prompt = Unicode(default_in_prompt, config=True)
94 in_prompt = Unicode(default_in_prompt, config=True)
95 out_prompt = Unicode(default_out_prompt, config=True)
95 out_prompt = Unicode(default_out_prompt, config=True)
96 input_sep = Unicode(default_input_sep, config=True)
96 input_sep = Unicode(default_input_sep, config=True)
97 output_sep = Unicode(default_output_sep, config=True)
97 output_sep = Unicode(default_output_sep, config=True)
98 output_sep2 = Unicode(default_output_sep2, config=True)
98 output_sep2 = Unicode(default_output_sep2, config=True)
99
99
100 # FrontendWidget protected class variables.
100 # FrontendWidget protected class variables.
101 _input_splitter_class = IPythonInputSplitter
101 _input_splitter_class = IPythonInputSplitter
102 _prompt_transformer = IPythonInputSplitter(physical_line_transforms=[ipy_prompt()],
102 _prompt_transformer = IPythonInputSplitter(physical_line_transforms=[ipy_prompt()],
103 logical_line_transforms=[],
103 logical_line_transforms=[],
104 python_line_transforms=[],
104 python_line_transforms=[],
105 )
105 )
106
106
107 # IPythonWidget protected class variables.
107 # IPythonWidget protected class variables.
108 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
108 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
109 _payload_source_edit = 'edit_magic'
109 _payload_source_edit = 'edit_magic'
110 _payload_source_exit = 'ask_exit'
110 _payload_source_exit = 'ask_exit'
111 _payload_source_next_input = 'set_next_input'
111 _payload_source_next_input = 'set_next_input'
112 _payload_source_page = 'page'
112 _payload_source_page = 'page'
113 _retrying_history_request = False
113 _retrying_history_request = False
114
114
115 #---------------------------------------------------------------------------
115 #---------------------------------------------------------------------------
116 # 'object' interface
116 # 'object' interface
117 #---------------------------------------------------------------------------
117 #---------------------------------------------------------------------------
118
118
119 def __init__(self, *args, **kw):
119 def __init__(self, *args, **kw):
120 super(IPythonWidget, self).__init__(*args, **kw)
120 super(IPythonWidget, self).__init__(*args, **kw)
121
121
122 # IPythonWidget protected variables.
122 # IPythonWidget protected variables.
123 self._payload_handlers = {
123 self._payload_handlers = {
124 self._payload_source_edit : self._handle_payload_edit,
124 self._payload_source_edit : self._handle_payload_edit,
125 self._payload_source_exit : self._handle_payload_exit,
125 self._payload_source_exit : self._handle_payload_exit,
126 self._payload_source_page : self._handle_payload_page,
126 self._payload_source_page : self._handle_payload_page,
127 self._payload_source_next_input : self._handle_payload_next_input }
127 self._payload_source_next_input : self._handle_payload_next_input }
128 self._previous_prompt_obj = None
128 self._previous_prompt_obj = None
129 self._keep_kernel_on_exit = None
129 self._keep_kernel_on_exit = None
130
130
131 # Initialize widget styling.
131 # Initialize widget styling.
132 if self.style_sheet:
132 if self.style_sheet:
133 self._style_sheet_changed()
133 self._style_sheet_changed()
134 self._syntax_style_changed()
134 self._syntax_style_changed()
135 else:
135 else:
136 self.set_default_style()
136 self.set_default_style()
137
137
138 self._guiref_loaded = False
138 self._guiref_loaded = False
139
139
140 #---------------------------------------------------------------------------
140 #---------------------------------------------------------------------------
141 # 'BaseFrontendMixin' abstract interface
141 # 'BaseFrontendMixin' abstract interface
142 #---------------------------------------------------------------------------
142 #---------------------------------------------------------------------------
143
144 def _handle_complete_reply(self, rep):
143 def _handle_complete_reply(self, rep):
145 """ Reimplemented to support IPython's improved completion machinery.
144 """ Reimplemented to support IPython's improved completion machinery.
146 """
145 """
147 self.log.debug("complete: %s", rep.get('content', ''))
146 self.log.debug("complete: %s", rep.get('content', ''))
148 cursor = self._get_cursor()
147 cursor = self._get_cursor()
149 info = self._request_info.get('complete')
148 info = self._request_info.get('complete')
150 if info and info.id == rep['parent_header']['msg_id'] and \
149 if info and info.id == rep['parent_header']['msg_id'] and \
151 info.pos == cursor.position():
150 info.pos == cursor.position():
152 matches = rep['content']['matches']
151 matches = rep['content']['matches']
153 text = rep['content']['matched_text']
152 text = rep['content']['matched_text']
154 offset = len(text)
153 offset = len(text)
155
154
156 # Clean up matches with period and path separators if the matched
155 # Clean up matches with period and path separators if the matched
157 # text has not been transformed. This is done by truncating all
156 # text has not been transformed. This is done by truncating all
158 # but the last component and then suitably decreasing the offset
157 # but the last component and then suitably decreasing the offset
159 # between the current cursor position and the start of completion.
158 # between the current cursor position and the start of completion.
160 if len(matches) > 1 and matches[0][:offset] == text:
159 if len(matches) > 1 and matches[0][:offset] == text:
161 parts = re.split(r'[./\\]', text)
160 parts = re.split(r'[./\\]', text)
162 sep_count = len(parts) - 1
161 sep_count = len(parts) - 1
163 if sep_count:
162 if sep_count:
164 chop_length = sum(map(len, parts[:sep_count])) + sep_count
163 chop_length = sum(map(len, parts[:sep_count])) + sep_count
165 matches = [ match[chop_length:] for match in matches ]
164 matches = [ match[chop_length:] for match in matches ]
166 offset -= chop_length
165 offset -= chop_length
167
166
168 # Move the cursor to the start of the match and complete.
167 # Move the cursor to the start of the match and complete.
169 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
168 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
170 self._complete_with_items(cursor, matches)
169 self._complete_with_items(cursor, matches)
171
170
172 def _handle_execute_reply(self, msg):
171 def _handle_execute_reply(self, msg):
173 """ Reimplemented to support prompt requests.
172 """ Reimplemented to support prompt requests.
174 """
173 """
175 msg_id = msg['parent_header'].get('msg_id')
174 msg_id = msg['parent_header'].get('msg_id')
176 info = self._request_info['execute'].get(msg_id)
175 info = self._request_info['execute'].get(msg_id)
177 if info and info.kind == 'prompt':
176 if info and info.kind == 'prompt':
178 content = msg['content']
177 content = msg['content']
179 if content['status'] == 'aborted':
178 if content['status'] == 'aborted':
180 self._show_interpreter_prompt()
179 self._show_interpreter_prompt()
181 else:
180 else:
182 number = content['execution_count'] + 1
181 number = content['execution_count'] + 1
183 self._show_interpreter_prompt(number)
182 self._show_interpreter_prompt(number)
184 self._request_info['execute'].pop(msg_id)
183 self._request_info['execute'].pop(msg_id)
185 else:
184 else:
186 super(IPythonWidget, self)._handle_execute_reply(msg)
185 super(IPythonWidget, self)._handle_execute_reply(msg)
187
186
188 def _handle_history_reply(self, msg):
187 def _handle_history_reply(self, msg):
189 """ Implemented to handle history tail replies, which are only supported
188 """ Implemented to handle history tail replies, which are only supported
190 by the IPython kernel.
189 by the IPython kernel.
191 """
190 """
192 content = msg['content']
191 content = msg['content']
193 if 'history' not in content:
192 if 'history' not in content:
194 self.log.error("History request failed: %r"%content)
193 self.log.error("History request failed: %r"%content)
195 if content.get('status', '') == 'aborted' and \
194 if content.get('status', '') == 'aborted' and \
196 not self._retrying_history_request:
195 not self._retrying_history_request:
197 # a *different* action caused this request to be aborted, so
196 # a *different* action caused this request to be aborted, so
198 # we should try again.
197 # we should try again.
199 self.log.error("Retrying aborted history request")
198 self.log.error("Retrying aborted history request")
200 # prevent multiple retries of aborted requests:
199 # prevent multiple retries of aborted requests:
201 self._retrying_history_request = True
200 self._retrying_history_request = True
202 # wait out the kernel's queue flush, which is currently timed at 0.1s
201 # wait out the kernel's queue flush, which is currently timed at 0.1s
203 time.sleep(0.25)
202 time.sleep(0.25)
204 self.kernel_client.shell_channel.history(hist_access_type='tail',n=1000)
203 self.kernel_client.shell_channel.history(hist_access_type='tail',n=1000)
205 else:
204 else:
206 self._retrying_history_request = False
205 self._retrying_history_request = False
207 return
206 return
208 # reset retry flag
207 # reset retry flag
209 self._retrying_history_request = False
208 self._retrying_history_request = False
210 history_items = content['history']
209 history_items = content['history']
211 self.log.debug("Received history reply with %i entries", len(history_items))
210 self.log.debug("Received history reply with %i entries", len(history_items))
212 items = []
211 items = []
213 last_cell = u""
212 last_cell = u""
214 for _, _, cell in history_items:
213 for _, _, cell in history_items:
215 cell = cell.rstrip()
214 cell = cell.rstrip()
216 if cell != last_cell:
215 if cell != last_cell:
217 items.append(cell)
216 items.append(cell)
218 last_cell = cell
217 last_cell = cell
219 self._set_history(items)
218 self._set_history(items)
220
219
221 def _handle_pyout(self, msg):
220 def _handle_pyout(self, msg):
222 """ Reimplemented for IPython-style "display hook".
221 """ Reimplemented for IPython-style "display hook".
223 """
222 """
224 self.log.debug("pyout: %s", msg.get('content', ''))
223 self.log.debug("pyout: %s", msg.get('content', ''))
225 if not self._hidden and self._is_from_this_session(msg):
224 if not self._hidden and self._is_from_this_session(msg):
225 self.flush_clearoutput()
226 content = msg['content']
226 content = msg['content']
227 prompt_number = content.get('execution_count', 0)
227 prompt_number = content.get('execution_count', 0)
228 data = content['data']
228 data = content['data']
229 if 'text/html' in data:
229 if 'text/html' in data:
230 self._append_plain_text(self.output_sep, True)
230 self._append_plain_text(self.output_sep, True)
231 self._append_html(self._make_out_prompt(prompt_number), True)
231 self._append_html(self._make_out_prompt(prompt_number), True)
232 html = data['text/html']
232 html = data['text/html']
233 self._append_plain_text('\n', True)
233 self._append_plain_text('\n', True)
234 self._append_html(html + self.output_sep2, True)
234 self._append_html(html + self.output_sep2, True)
235 elif 'text/plain' in data:
235 elif 'text/plain' in data:
236 self._append_plain_text(self.output_sep, True)
236 self._append_plain_text(self.output_sep, True)
237 self._append_html(self._make_out_prompt(prompt_number), True)
237 self._append_html(self._make_out_prompt(prompt_number), True)
238 text = data['text/plain']
238 text = data['text/plain']
239 # If the repr is multiline, make sure we start on a new line,
239 # If the repr is multiline, make sure we start on a new line,
240 # so that its lines are aligned.
240 # so that its lines are aligned.
241 if "\n" in text and not self.output_sep.endswith("\n"):
241 if "\n" in text and not self.output_sep.endswith("\n"):
242 self._append_plain_text('\n', True)
242 self._append_plain_text('\n', True)
243 self._append_plain_text(text + self.output_sep2, True)
243 self._append_plain_text(text + self.output_sep2, True)
244
244
245 def _handle_display_data(self, msg):
245 def _handle_display_data(self, msg):
246 """ The base handler for the ``display_data`` message.
246 """ The base handler for the ``display_data`` message.
247 """
247 """
248 self.log.debug("display: %s", msg.get('content', ''))
248 self.log.debug("display: %s", msg.get('content', ''))
249 # For now, we don't display data from other frontends, but we
249 # For now, we don't display data from other frontends, but we
250 # eventually will as this allows all frontends to monitor the display
250 # eventually will as this allows all frontends to monitor the display
251 # data. But we need to figure out how to handle this in the GUI.
251 # data. But we need to figure out how to handle this in the GUI.
252 if not self._hidden and self._is_from_this_session(msg):
252 if not self._hidden and self._is_from_this_session(msg):
253 self.flush_clearoutput()
253 source = msg['content']['source']
254 source = msg['content']['source']
254 data = msg['content']['data']
255 data = msg['content']['data']
255 metadata = msg['content']['metadata']
256 metadata = msg['content']['metadata']
256 # In the regular IPythonWidget, we simply print the plain text
257 # In the regular IPythonWidget, we simply print the plain text
257 # representation.
258 # representation.
258 if 'text/html' in data:
259 if 'text/html' in data:
259 html = data['text/html']
260 html = data['text/html']
260 self._append_html(html, True)
261 self._append_html(html, True)
261 elif 'text/plain' in data:
262 elif 'text/plain' in data:
262 text = data['text/plain']
263 text = data['text/plain']
263 self._append_plain_text(text, True)
264 self._append_plain_text(text, True)
264 # This newline seems to be needed for text and html output.
265 # This newline seems to be needed for text and html output.
265 self._append_plain_text(u'\n', True)
266 self._append_plain_text(u'\n', True)
266
267
267 def _handle_kernel_info_reply(self, rep):
268 def _handle_kernel_info_reply(self, rep):
268 """ Handle kernel info replies.
269 """ Handle kernel info replies.
269 """
270 """
270 if not self._guiref_loaded:
271 if not self._guiref_loaded:
271 if rep['content'].get('language') == 'python':
272 if rep['content'].get('language') == 'python':
272 self._load_guiref_magic()
273 self._load_guiref_magic()
273 self._guiref_loaded = True
274 self._guiref_loaded = True
274
275
275 def _started_channels(self):
276 def _started_channels(self):
276 """Reimplemented to make a history request and load %guiref."""
277 """Reimplemented to make a history request and load %guiref."""
277 super(IPythonWidget, self)._started_channels()
278 super(IPythonWidget, self)._started_channels()
278
279
279 # The reply will trigger %guiref load provided language=='python'
280 # The reply will trigger %guiref load provided language=='python'
280 self.kernel_client.kernel_info()
281 self.kernel_client.kernel_info()
281
282
282 self.kernel_client.shell_channel.history(hist_access_type='tail',
283 self.kernel_client.shell_channel.history(hist_access_type='tail',
283 n=1000)
284 n=1000)
284
285
285 def _started_kernel(self):
286 def _started_kernel(self):
286 """Load %guiref when the kernel starts (if channels are also started).
287 """Load %guiref when the kernel starts (if channels are also started).
287
288
288 Principally triggered by kernel restart.
289 Principally triggered by kernel restart.
289 """
290 """
290 if self.kernel_client.shell_channel is not None:
291 if self.kernel_client.shell_channel is not None:
291 self._load_guiref_magic()
292 self._load_guiref_magic()
292
293
293 def _load_guiref_magic(self):
294 def _load_guiref_magic(self):
294 """Load %guiref magic."""
295 """Load %guiref magic."""
295 self.kernel_client.shell_channel.execute('\n'.join([
296 self.kernel_client.shell_channel.execute('\n'.join([
296 "try:",
297 "try:",
297 " _usage",
298 " _usage",
298 "except:",
299 "except:",
299 " from IPython.core import usage as _usage",
300 " from IPython.core import usage as _usage",
300 " get_ipython().register_magic_function(_usage.page_guiref, 'line', 'guiref')",
301 " get_ipython().register_magic_function(_usage.page_guiref, 'line', 'guiref')",
301 " del _usage",
302 " del _usage",
302 ]), silent=True)
303 ]), silent=True)
303
304
304 #---------------------------------------------------------------------------
305 #---------------------------------------------------------------------------
305 # 'ConsoleWidget' public interface
306 # 'ConsoleWidget' public interface
306 #---------------------------------------------------------------------------
307 #---------------------------------------------------------------------------
307
308
308 #---------------------------------------------------------------------------
309 #---------------------------------------------------------------------------
309 # 'FrontendWidget' public interface
310 # 'FrontendWidget' public interface
310 #---------------------------------------------------------------------------
311 #---------------------------------------------------------------------------
311
312
312 def execute_file(self, path, hidden=False):
313 def execute_file(self, path, hidden=False):
313 """ Reimplemented to use the 'run' magic.
314 """ Reimplemented to use the 'run' magic.
314 """
315 """
315 # Use forward slashes on Windows to avoid escaping each separator.
316 # Use forward slashes on Windows to avoid escaping each separator.
316 if sys.platform == 'win32':
317 if sys.platform == 'win32':
317 path = os.path.normpath(path).replace('\\', '/')
318 path = os.path.normpath(path).replace('\\', '/')
318
319
319 # Perhaps we should not be using %run directly, but while we
320 # Perhaps we should not be using %run directly, but while we
320 # are, it is necessary to quote or escape filenames containing spaces
321 # are, it is necessary to quote or escape filenames containing spaces
321 # or quotes.
322 # or quotes.
322
323
323 # In earlier code here, to minimize escaping, we sometimes quoted the
324 # In earlier code here, to minimize escaping, we sometimes quoted the
324 # filename with single quotes. But to do this, this code must be
325 # filename with single quotes. But to do this, this code must be
325 # platform-aware, because run uses shlex rather than python string
326 # platform-aware, because run uses shlex rather than python string
326 # parsing, so that:
327 # parsing, so that:
327 # * In Win: single quotes can be used in the filename without quoting,
328 # * In Win: single quotes can be used in the filename without quoting,
328 # and we cannot use single quotes to quote the filename.
329 # and we cannot use single quotes to quote the filename.
329 # * In *nix: we can escape double quotes in a double quoted filename,
330 # * In *nix: we can escape double quotes in a double quoted filename,
330 # but can't escape single quotes in a single quoted filename.
331 # but can't escape single quotes in a single quoted filename.
331
332
332 # So to keep this code non-platform-specific and simple, we now only
333 # So to keep this code non-platform-specific and simple, we now only
333 # use double quotes to quote filenames, and escape when needed:
334 # use double quotes to quote filenames, and escape when needed:
334 if ' ' in path or "'" in path or '"' in path:
335 if ' ' in path or "'" in path or '"' in path:
335 path = '"%s"' % path.replace('"', '\\"')
336 path = '"%s"' % path.replace('"', '\\"')
336 self.execute('%%run %s' % path, hidden=hidden)
337 self.execute('%%run %s' % path, hidden=hidden)
337
338
338 #---------------------------------------------------------------------------
339 #---------------------------------------------------------------------------
339 # 'FrontendWidget' protected interface
340 # 'FrontendWidget' protected interface
340 #---------------------------------------------------------------------------
341 #---------------------------------------------------------------------------
341
342
342 def _complete(self):
343 def _complete(self):
343 """ Reimplemented to support IPython's improved completion machinery.
344 """ Reimplemented to support IPython's improved completion machinery.
344 """
345 """
345 # We let the kernel split the input line, so we *always* send an empty
346 # We let the kernel split the input line, so we *always* send an empty
346 # text field. Readline-based frontends do get a real text field which
347 # text field. Readline-based frontends do get a real text field which
347 # they can use.
348 # they can use.
348 text = ''
349 text = ''
349
350
350 # Send the completion request to the kernel
351 # Send the completion request to the kernel
351 msg_id = self.kernel_client.shell_channel.complete(
352 msg_id = self.kernel_client.shell_channel.complete(
352 text, # text
353 text, # text
353 self._get_input_buffer_cursor_line(), # line
354 self._get_input_buffer_cursor_line(), # line
354 self._get_input_buffer_cursor_column(), # cursor_pos
355 self._get_input_buffer_cursor_column(), # cursor_pos
355 self.input_buffer) # block
356 self.input_buffer) # block
356 pos = self._get_cursor().position()
357 pos = self._get_cursor().position()
357 info = self._CompletionRequest(msg_id, pos)
358 info = self._CompletionRequest(msg_id, pos)
358 self._request_info['complete'] = info
359 self._request_info['complete'] = info
359
360
360 def _process_execute_error(self, msg):
361 def _process_execute_error(self, msg):
361 """ Reimplemented for IPython-style traceback formatting.
362 """ Reimplemented for IPython-style traceback formatting.
362 """
363 """
363 content = msg['content']
364 content = msg['content']
364 traceback = '\n'.join(content['traceback']) + '\n'
365 traceback = '\n'.join(content['traceback']) + '\n'
365 if False:
366 if False:
366 # FIXME: For now, tracebacks come as plain text, so we can't use
367 # FIXME: For now, tracebacks come as plain text, so we can't use
367 # the html renderer yet. Once we refactor ultratb to produce
368 # the html renderer yet. Once we refactor ultratb to produce
368 # properly styled tracebacks, this branch should be the default
369 # properly styled tracebacks, this branch should be the default
369 traceback = traceback.replace(' ', '&nbsp;')
370 traceback = traceback.replace(' ', '&nbsp;')
370 traceback = traceback.replace('\n', '<br/>')
371 traceback = traceback.replace('\n', '<br/>')
371
372
372 ename = content['ename']
373 ename = content['ename']
373 ename_styled = '<span class="error">%s</span>' % ename
374 ename_styled = '<span class="error">%s</span>' % ename
374 traceback = traceback.replace(ename, ename_styled)
375 traceback = traceback.replace(ename, ename_styled)
375
376
376 self._append_html(traceback)
377 self._append_html(traceback)
377 else:
378 else:
378 # This is the fallback for now, using plain text with ansi escapes
379 # This is the fallback for now, using plain text with ansi escapes
379 self._append_plain_text(traceback)
380 self._append_plain_text(traceback)
380
381
381 def _process_execute_payload(self, item):
382 def _process_execute_payload(self, item):
382 """ Reimplemented to dispatch payloads to handler methods.
383 """ Reimplemented to dispatch payloads to handler methods.
383 """
384 """
384 handler = self._payload_handlers.get(item['source'])
385 handler = self._payload_handlers.get(item['source'])
385 if handler is None:
386 if handler is None:
386 # We have no handler for this type of payload, simply ignore it
387 # We have no handler for this type of payload, simply ignore it
387 return False
388 return False
388 else:
389 else:
389 handler(item)
390 handler(item)
390 return True
391 return True
391
392
392 def _show_interpreter_prompt(self, number=None):
393 def _show_interpreter_prompt(self, number=None):
393 """ Reimplemented for IPython-style prompts.
394 """ Reimplemented for IPython-style prompts.
394 """
395 """
395 # If a number was not specified, make a prompt number request.
396 # If a number was not specified, make a prompt number request.
396 if number is None:
397 if number is None:
397 msg_id = self.kernel_client.shell_channel.execute('', silent=True)
398 msg_id = self.kernel_client.shell_channel.execute('', silent=True)
398 info = self._ExecutionRequest(msg_id, 'prompt')
399 info = self._ExecutionRequest(msg_id, 'prompt')
399 self._request_info['execute'][msg_id] = info
400 self._request_info['execute'][msg_id] = info
400 return
401 return
401
402
402 # Show a new prompt and save information about it so that it can be
403 # Show a new prompt and save information about it so that it can be
403 # updated later if the prompt number turns out to be wrong.
404 # updated later if the prompt number turns out to be wrong.
404 self._prompt_sep = self.input_sep
405 self._prompt_sep = self.input_sep
405 self._show_prompt(self._make_in_prompt(number), html=True)
406 self._show_prompt(self._make_in_prompt(number), html=True)
406 block = self._control.document().lastBlock()
407 block = self._control.document().lastBlock()
407 length = len(self._prompt)
408 length = len(self._prompt)
408 self._previous_prompt_obj = self._PromptBlock(block, length, number)
409 self._previous_prompt_obj = self._PromptBlock(block, length, number)
409
410
410 # Update continuation prompt to reflect (possibly) new prompt length.
411 # Update continuation prompt to reflect (possibly) new prompt length.
411 self._set_continuation_prompt(
412 self._set_continuation_prompt(
412 self._make_continuation_prompt(self._prompt), html=True)
413 self._make_continuation_prompt(self._prompt), html=True)
413
414
414 def _show_interpreter_prompt_for_reply(self, msg):
415 def _show_interpreter_prompt_for_reply(self, msg):
415 """ Reimplemented for IPython-style prompts.
416 """ Reimplemented for IPython-style prompts.
416 """
417 """
417 # Update the old prompt number if necessary.
418 # Update the old prompt number if necessary.
418 content = msg['content']
419 content = msg['content']
419 # abort replies do not have any keys:
420 # abort replies do not have any keys:
420 if content['status'] == 'aborted':
421 if content['status'] == 'aborted':
421 if self._previous_prompt_obj:
422 if self._previous_prompt_obj:
422 previous_prompt_number = self._previous_prompt_obj.number
423 previous_prompt_number = self._previous_prompt_obj.number
423 else:
424 else:
424 previous_prompt_number = 0
425 previous_prompt_number = 0
425 else:
426 else:
426 previous_prompt_number = content['execution_count']
427 previous_prompt_number = content['execution_count']
427 if self._previous_prompt_obj and \
428 if self._previous_prompt_obj and \
428 self._previous_prompt_obj.number != previous_prompt_number:
429 self._previous_prompt_obj.number != previous_prompt_number:
429 block = self._previous_prompt_obj.block
430 block = self._previous_prompt_obj.block
430
431
431 # Make sure the prompt block has not been erased.
432 # Make sure the prompt block has not been erased.
432 if block.isValid() and block.text():
433 if block.isValid() and block.text():
433
434
434 # Remove the old prompt and insert a new prompt.
435 # Remove the old prompt and insert a new prompt.
435 cursor = QtGui.QTextCursor(block)
436 cursor = QtGui.QTextCursor(block)
436 cursor.movePosition(QtGui.QTextCursor.Right,
437 cursor.movePosition(QtGui.QTextCursor.Right,
437 QtGui.QTextCursor.KeepAnchor,
438 QtGui.QTextCursor.KeepAnchor,
438 self._previous_prompt_obj.length)
439 self._previous_prompt_obj.length)
439 prompt = self._make_in_prompt(previous_prompt_number)
440 prompt = self._make_in_prompt(previous_prompt_number)
440 self._prompt = self._insert_html_fetching_plain_text(
441 self._prompt = self._insert_html_fetching_plain_text(
441 cursor, prompt)
442 cursor, prompt)
442
443
443 # When the HTML is inserted, Qt blows away the syntax
444 # When the HTML is inserted, Qt blows away the syntax
444 # highlighting for the line, so we need to rehighlight it.
445 # highlighting for the line, so we need to rehighlight it.
445 self._highlighter.rehighlightBlock(cursor.block())
446 self._highlighter.rehighlightBlock(cursor.block())
446
447
447 self._previous_prompt_obj = None
448 self._previous_prompt_obj = None
448
449
449 # Show a new prompt with the kernel's estimated prompt number.
450 # Show a new prompt with the kernel's estimated prompt number.
450 self._show_interpreter_prompt(previous_prompt_number + 1)
451 self._show_interpreter_prompt(previous_prompt_number + 1)
451
452
452 #---------------------------------------------------------------------------
453 #---------------------------------------------------------------------------
453 # 'IPythonWidget' interface
454 # 'IPythonWidget' interface
454 #---------------------------------------------------------------------------
455 #---------------------------------------------------------------------------
455
456
456 def set_default_style(self, colors='lightbg'):
457 def set_default_style(self, colors='lightbg'):
457 """ Sets the widget style to the class defaults.
458 """ Sets the widget style to the class defaults.
458
459
459 Parameters
460 Parameters
460 ----------
461 ----------
461 colors : str, optional (default lightbg)
462 colors : str, optional (default lightbg)
462 Whether to use the default IPython light background or dark
463 Whether to use the default IPython light background or dark
463 background or B&W style.
464 background or B&W style.
464 """
465 """
465 colors = colors.lower()
466 colors = colors.lower()
466 if colors=='lightbg':
467 if colors=='lightbg':
467 self.style_sheet = styles.default_light_style_sheet
468 self.style_sheet = styles.default_light_style_sheet
468 self.syntax_style = styles.default_light_syntax_style
469 self.syntax_style = styles.default_light_syntax_style
469 elif colors=='linux':
470 elif colors=='linux':
470 self.style_sheet = styles.default_dark_style_sheet
471 self.style_sheet = styles.default_dark_style_sheet
471 self.syntax_style = styles.default_dark_syntax_style
472 self.syntax_style = styles.default_dark_syntax_style
472 elif colors=='nocolor':
473 elif colors=='nocolor':
473 self.style_sheet = styles.default_bw_style_sheet
474 self.style_sheet = styles.default_bw_style_sheet
474 self.syntax_style = styles.default_bw_syntax_style
475 self.syntax_style = styles.default_bw_syntax_style
475 else:
476 else:
476 raise KeyError("No such color scheme: %s"%colors)
477 raise KeyError("No such color scheme: %s"%colors)
477
478
478 #---------------------------------------------------------------------------
479 #---------------------------------------------------------------------------
479 # 'IPythonWidget' protected interface
480 # 'IPythonWidget' protected interface
480 #---------------------------------------------------------------------------
481 #---------------------------------------------------------------------------
481
482
482 def _edit(self, filename, line=None):
483 def _edit(self, filename, line=None):
483 """ Opens a Python script for editing.
484 """ Opens a Python script for editing.
484
485
485 Parameters
486 Parameters
486 ----------
487 ----------
487 filename : str
488 filename : str
488 A path to a local system file.
489 A path to a local system file.
489
490
490 line : int, optional
491 line : int, optional
491 A line of interest in the file.
492 A line of interest in the file.
492 """
493 """
493 if self.custom_edit:
494 if self.custom_edit:
494 self.custom_edit_requested.emit(filename, line)
495 self.custom_edit_requested.emit(filename, line)
495 elif not self.editor:
496 elif not self.editor:
496 self._append_plain_text('No default editor available.\n'
497 self._append_plain_text('No default editor available.\n'
497 'Specify a GUI text editor in the `IPythonWidget.editor` '
498 'Specify a GUI text editor in the `IPythonWidget.editor` '
498 'configurable to enable the %edit magic')
499 'configurable to enable the %edit magic')
499 else:
500 else:
500 try:
501 try:
501 filename = '"%s"' % filename
502 filename = '"%s"' % filename
502 if line and self.editor_line:
503 if line and self.editor_line:
503 command = self.editor_line.format(filename=filename,
504 command = self.editor_line.format(filename=filename,
504 line=line)
505 line=line)
505 else:
506 else:
506 try:
507 try:
507 command = self.editor.format()
508 command = self.editor.format()
508 except KeyError:
509 except KeyError:
509 command = self.editor.format(filename=filename)
510 command = self.editor.format(filename=filename)
510 else:
511 else:
511 command += ' ' + filename
512 command += ' ' + filename
512 except KeyError:
513 except KeyError:
513 self._append_plain_text('Invalid editor command.\n')
514 self._append_plain_text('Invalid editor command.\n')
514 else:
515 else:
515 try:
516 try:
516 Popen(command, shell=True)
517 Popen(command, shell=True)
517 except OSError:
518 except OSError:
518 msg = 'Opening editor with command "%s" failed.\n'
519 msg = 'Opening editor with command "%s" failed.\n'
519 self._append_plain_text(msg % command)
520 self._append_plain_text(msg % command)
520
521
521 def _make_in_prompt(self, number):
522 def _make_in_prompt(self, number):
522 """ Given a prompt number, returns an HTML In prompt.
523 """ Given a prompt number, returns an HTML In prompt.
523 """
524 """
524 try:
525 try:
525 body = self.in_prompt % number
526 body = self.in_prompt % number
526 except TypeError:
527 except TypeError:
527 # allow in_prompt to leave out number, e.g. '>>> '
528 # allow in_prompt to leave out number, e.g. '>>> '
528 body = self.in_prompt
529 body = self.in_prompt
529 return '<span class="in-prompt">%s</span>' % body
530 return '<span class="in-prompt">%s</span>' % body
530
531
531 def _make_continuation_prompt(self, prompt):
532 def _make_continuation_prompt(self, prompt):
532 """ Given a plain text version of an In prompt, returns an HTML
533 """ Given a plain text version of an In prompt, returns an HTML
533 continuation prompt.
534 continuation prompt.
534 """
535 """
535 end_chars = '...: '
536 end_chars = '...: '
536 space_count = len(prompt.lstrip('\n')) - len(end_chars)
537 space_count = len(prompt.lstrip('\n')) - len(end_chars)
537 body = '&nbsp;' * space_count + end_chars
538 body = '&nbsp;' * space_count + end_chars
538 return '<span class="in-prompt">%s</span>' % body
539 return '<span class="in-prompt">%s</span>' % body
539
540
540 def _make_out_prompt(self, number):
541 def _make_out_prompt(self, number):
541 """ Given a prompt number, returns an HTML Out prompt.
542 """ Given a prompt number, returns an HTML Out prompt.
542 """
543 """
543 body = self.out_prompt % number
544 body = self.out_prompt % number
544 return '<span class="out-prompt">%s</span>' % body
545 return '<span class="out-prompt">%s</span>' % body
545
546
546 #------ Payload handlers --------------------------------------------------
547 #------ Payload handlers --------------------------------------------------
547
548
548 # Payload handlers with a generic interface: each takes the opaque payload
549 # Payload handlers with a generic interface: each takes the opaque payload
549 # dict, unpacks it and calls the underlying functions with the necessary
550 # dict, unpacks it and calls the underlying functions with the necessary
550 # arguments.
551 # arguments.
551
552
552 def _handle_payload_edit(self, item):
553 def _handle_payload_edit(self, item):
553 self._edit(item['filename'], item['line_number'])
554 self._edit(item['filename'], item['line_number'])
554
555
555 def _handle_payload_exit(self, item):
556 def _handle_payload_exit(self, item):
556 self._keep_kernel_on_exit = item['keepkernel']
557 self._keep_kernel_on_exit = item['keepkernel']
557 self.exit_requested.emit(self)
558 self.exit_requested.emit(self)
558
559
559 def _handle_payload_next_input(self, item):
560 def _handle_payload_next_input(self, item):
560 self.input_buffer = item['text']
561 self.input_buffer = item['text']
561
562
562 def _handle_payload_page(self, item):
563 def _handle_payload_page(self, item):
563 # Since the plain text widget supports only a very small subset of HTML
564 # Since the plain text widget supports only a very small subset of HTML
564 # and we have no control over the HTML source, we only page HTML
565 # and we have no control over the HTML source, we only page HTML
565 # payloads in the rich text widget.
566 # payloads in the rich text widget.
566 if item['html'] and self.kind == 'rich':
567 if item['html'] and self.kind == 'rich':
567 self._page(item['html'], html=True)
568 self._page(item['html'], html=True)
568 else:
569 else:
569 self._page(item['text'], html=False)
570 self._page(item['text'], html=False)
570
571
571 #------ Trait change handlers --------------------------------------------
572 #------ Trait change handlers --------------------------------------------
572
573
573 def _style_sheet_changed(self):
574 def _style_sheet_changed(self):
574 """ Set the style sheets of the underlying widgets.
575 """ Set the style sheets of the underlying widgets.
575 """
576 """
576 self.setStyleSheet(self.style_sheet)
577 self.setStyleSheet(self.style_sheet)
577 if self._control is not None:
578 if self._control is not None:
578 self._control.document().setDefaultStyleSheet(self.style_sheet)
579 self._control.document().setDefaultStyleSheet(self.style_sheet)
579 bg_color = self._control.palette().window().color()
580 bg_color = self._control.palette().window().color()
580 self._ansi_processor.set_background_color(bg_color)
581 self._ansi_processor.set_background_color(bg_color)
581
582
582 if self._page_control is not None:
583 if self._page_control is not None:
583 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
584 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
584
585
585
586
586
587
587 def _syntax_style_changed(self):
588 def _syntax_style_changed(self):
588 """ Set the style for the syntax highlighter.
589 """ Set the style for the syntax highlighter.
589 """
590 """
590 if self._highlighter is None:
591 if self._highlighter is None:
591 # ignore premature calls
592 # ignore premature calls
592 return
593 return
593 if self.syntax_style:
594 if self.syntax_style:
594 self._highlighter.set_style(self.syntax_style)
595 self._highlighter.set_style(self.syntax_style)
595 else:
596 else:
596 self._highlighter.set_style_sheet(self.style_sheet)
597 self._highlighter.set_style_sheet(self.style_sheet)
597
598
598 #------ Trait default initializers -----------------------------------------
599 #------ Trait default initializers -----------------------------------------
599
600
600 def _banner_default(self):
601 def _banner_default(self):
601 from IPython.core.usage import default_gui_banner
602 from IPython.core.usage import default_gui_banner
602 return default_gui_banner
603 return default_gui_banner
@@ -1,341 +1,343 b''
1 #-----------------------------------------------------------------------------
1 #-----------------------------------------------------------------------------
2 # Copyright (c) 2010, IPython Development Team.
2 # Copyright (c) 2010, IPython Development Team.
3 #
3 #
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5 #
5 #
6 # The full license is in the file COPYING.txt, distributed with this software.
6 # The full license is in the file COPYING.txt, distributed with this software.
7 #-----------------------------------------------------------------------------
7 #-----------------------------------------------------------------------------
8
8
9 # Standard libary imports.
9 # Standard libary imports.
10 from base64 import decodestring
10 from base64 import decodestring
11 import os
11 import os
12 import re
12 import re
13
13
14 # System libary imports.
14 # System libary imports.
15 from IPython.external.qt import QtCore, QtGui
15 from IPython.external.qt import QtCore, QtGui
16
16
17 # Local imports
17 # Local imports
18 from IPython.utils.traitlets import Bool
18 from IPython.utils.traitlets import Bool
19 from IPython.qt.svg import save_svg, svg_to_clipboard, svg_to_image
19 from IPython.qt.svg import save_svg, svg_to_clipboard, svg_to_image
20 from .ipython_widget import IPythonWidget
20 from .ipython_widget import IPythonWidget
21
21
22
22
23 class RichIPythonWidget(IPythonWidget):
23 class RichIPythonWidget(IPythonWidget):
24 """ An IPythonWidget that supports rich text, including lists, images, and
24 """ An IPythonWidget that supports rich text, including lists, images, and
25 tables. Note that raw performance will be reduced compared to the plain
25 tables. Note that raw performance will be reduced compared to the plain
26 text version.
26 text version.
27 """
27 """
28
28
29 # RichIPythonWidget protected class variables.
29 # RichIPythonWidget protected class variables.
30 _payload_source_plot = 'IPython.kernel.zmq.pylab.backend_payload.add_plot_payload'
30 _payload_source_plot = 'IPython.kernel.zmq.pylab.backend_payload.add_plot_payload'
31 _jpg_supported = Bool(False)
31 _jpg_supported = Bool(False)
32
32
33 # Used to determine whether a given html export attempt has already
33 # Used to determine whether a given html export attempt has already
34 # displayed a warning about being unable to convert a png to svg.
34 # displayed a warning about being unable to convert a png to svg.
35 _svg_warning_displayed = False
35 _svg_warning_displayed = False
36
36
37 #---------------------------------------------------------------------------
37 #---------------------------------------------------------------------------
38 # 'object' interface
38 # 'object' interface
39 #---------------------------------------------------------------------------
39 #---------------------------------------------------------------------------
40
40
41 def __init__(self, *args, **kw):
41 def __init__(self, *args, **kw):
42 """ Create a RichIPythonWidget.
42 """ Create a RichIPythonWidget.
43 """
43 """
44 kw['kind'] = 'rich'
44 kw['kind'] = 'rich'
45 super(RichIPythonWidget, self).__init__(*args, **kw)
45 super(RichIPythonWidget, self).__init__(*args, **kw)
46
46
47 # Configure the ConsoleWidget HTML exporter for our formats.
47 # Configure the ConsoleWidget HTML exporter for our formats.
48 self._html_exporter.image_tag = self._get_image_tag
48 self._html_exporter.image_tag = self._get_image_tag
49
49
50 # Dictionary for resolving document resource names to SVG data.
50 # Dictionary for resolving document resource names to SVG data.
51 self._name_to_svg_map = {}
51 self._name_to_svg_map = {}
52
52
53 # Do we support jpg ?
53 # Do we support jpg ?
54 # it seems that sometime jpg support is a plugin of QT, so try to assume
54 # it seems that sometime jpg support is a plugin of QT, so try to assume
55 # it is not always supported.
55 # it is not always supported.
56 _supported_format = map(str, QtGui.QImageReader.supportedImageFormats())
56 _supported_format = map(str, QtGui.QImageReader.supportedImageFormats())
57 self._jpg_supported = 'jpeg' in _supported_format
57 self._jpg_supported = 'jpeg' in _supported_format
58
58
59
59
60 #---------------------------------------------------------------------------
60 #---------------------------------------------------------------------------
61 # 'ConsoleWidget' public interface overides
61 # 'ConsoleWidget' public interface overides
62 #---------------------------------------------------------------------------
62 #---------------------------------------------------------------------------
63
63
64 def export_html(self):
64 def export_html(self):
65 """ Shows a dialog to export HTML/XML in various formats.
65 """ Shows a dialog to export HTML/XML in various formats.
66
66
67 Overridden in order to reset the _svg_warning_displayed flag prior
67 Overridden in order to reset the _svg_warning_displayed flag prior
68 to the export running.
68 to the export running.
69 """
69 """
70 self._svg_warning_displayed = False
70 self._svg_warning_displayed = False
71 super(RichIPythonWidget, self).export_html()
71 super(RichIPythonWidget, self).export_html()
72
72
73
73
74 #---------------------------------------------------------------------------
74 #---------------------------------------------------------------------------
75 # 'ConsoleWidget' protected interface
75 # 'ConsoleWidget' protected interface
76 #---------------------------------------------------------------------------
76 #---------------------------------------------------------------------------
77
77
78 def _context_menu_make(self, pos):
78 def _context_menu_make(self, pos):
79 """ Reimplemented to return a custom context menu for images.
79 """ Reimplemented to return a custom context menu for images.
80 """
80 """
81 format = self._control.cursorForPosition(pos).charFormat()
81 format = self._control.cursorForPosition(pos).charFormat()
82 name = format.stringProperty(QtGui.QTextFormat.ImageName)
82 name = format.stringProperty(QtGui.QTextFormat.ImageName)
83 if name:
83 if name:
84 menu = QtGui.QMenu()
84 menu = QtGui.QMenu()
85
85
86 menu.addAction('Copy Image', lambda: self._copy_image(name))
86 menu.addAction('Copy Image', lambda: self._copy_image(name))
87 menu.addAction('Save Image As...', lambda: self._save_image(name))
87 menu.addAction('Save Image As...', lambda: self._save_image(name))
88 menu.addSeparator()
88 menu.addSeparator()
89
89
90 svg = self._name_to_svg_map.get(name, None)
90 svg = self._name_to_svg_map.get(name, None)
91 if svg is not None:
91 if svg is not None:
92 menu.addSeparator()
92 menu.addSeparator()
93 menu.addAction('Copy SVG', lambda: svg_to_clipboard(svg))
93 menu.addAction('Copy SVG', lambda: svg_to_clipboard(svg))
94 menu.addAction('Save SVG As...',
94 menu.addAction('Save SVG As...',
95 lambda: save_svg(svg, self._control))
95 lambda: save_svg(svg, self._control))
96 else:
96 else:
97 menu = super(RichIPythonWidget, self)._context_menu_make(pos)
97 menu = super(RichIPythonWidget, self)._context_menu_make(pos)
98 return menu
98 return menu
99
99
100 #---------------------------------------------------------------------------
100 #---------------------------------------------------------------------------
101 # 'BaseFrontendMixin' abstract interface
101 # 'BaseFrontendMixin' abstract interface
102 #---------------------------------------------------------------------------
102 #---------------------------------------------------------------------------
103 def _pre_image_append(self, msg, prompt_number):
103 def _pre_image_append(self, msg, prompt_number):
104 """ Append the Out[] prompt and make the output nicer
104 """ Append the Out[] prompt and make the output nicer
105
105
106 Shared code for some the following if statement
106 Shared code for some the following if statement
107 """
107 """
108 self.log.debug("pyout: %s", msg.get('content', ''))
108 self.log.debug("pyout: %s", msg.get('content', ''))
109 self._append_plain_text(self.output_sep, True)
109 self._append_plain_text(self.output_sep, True)
110 self._append_html(self._make_out_prompt(prompt_number), True)
110 self._append_html(self._make_out_prompt(prompt_number), True)
111 self._append_plain_text('\n', True)
111 self._append_plain_text('\n', True)
112
112
113 def _handle_pyout(self, msg):
113 def _handle_pyout(self, msg):
114 """ Overridden to handle rich data types, like SVG.
114 """ Overridden to handle rich data types, like SVG.
115 """
115 """
116 if not self._hidden and self._is_from_this_session(msg):
116 if not self._hidden and self._is_from_this_session(msg):
117 self.flush_clearoutput()
117 content = msg['content']
118 content = msg['content']
118 prompt_number = content.get('execution_count', 0)
119 prompt_number = content.get('execution_count', 0)
119 data = content['data']
120 data = content['data']
120 metadata = msg['content']['metadata']
121 metadata = msg['content']['metadata']
121 if 'image/svg+xml' in data:
122 if 'image/svg+xml' in data:
122 self._pre_image_append(msg, prompt_number)
123 self._pre_image_append(msg, prompt_number)
123 self._append_svg(data['image/svg+xml'], True)
124 self._append_svg(data['image/svg+xml'], True)
124 self._append_html(self.output_sep2, True)
125 self._append_html(self.output_sep2, True)
125 elif 'image/png' in data:
126 elif 'image/png' in data:
126 self._pre_image_append(msg, prompt_number)
127 self._pre_image_append(msg, prompt_number)
127 png = decodestring(data['image/png'].encode('ascii'))
128 png = decodestring(data['image/png'].encode('ascii'))
128 self._append_png(png, True, metadata=metadata.get('image/png', None))
129 self._append_png(png, True, metadata=metadata.get('image/png', None))
129 self._append_html(self.output_sep2, True)
130 self._append_html(self.output_sep2, True)
130 elif 'image/jpeg' in data and self._jpg_supported:
131 elif 'image/jpeg' in data and self._jpg_supported:
131 self._pre_image_append(msg, prompt_number)
132 self._pre_image_append(msg, prompt_number)
132 jpg = decodestring(data['image/jpeg'].encode('ascii'))
133 jpg = decodestring(data['image/jpeg'].encode('ascii'))
133 self._append_jpg(jpg, True, metadata=metadata.get('image/jpeg', None))
134 self._append_jpg(jpg, True, metadata=metadata.get('image/jpeg', None))
134 self._append_html(self.output_sep2, True)
135 self._append_html(self.output_sep2, True)
135 else:
136 else:
136 # Default back to the plain text representation.
137 # Default back to the plain text representation.
137 return super(RichIPythonWidget, self)._handle_pyout(msg)
138 return super(RichIPythonWidget, self)._handle_pyout(msg)
138
139
139 def _handle_display_data(self, msg):
140 def _handle_display_data(self, msg):
140 """ Overridden to handle rich data types, like SVG.
141 """ Overridden to handle rich data types, like SVG.
141 """
142 """
142 if not self._hidden and self._is_from_this_session(msg):
143 if not self._hidden and self._is_from_this_session(msg):
144 self.flush_clearoutput()
143 source = msg['content']['source']
145 source = msg['content']['source']
144 data = msg['content']['data']
146 data = msg['content']['data']
145 metadata = msg['content']['metadata']
147 metadata = msg['content']['metadata']
146 # Try to use the svg or html representations.
148 # Try to use the svg or html representations.
147 # FIXME: Is this the right ordering of things to try?
149 # FIXME: Is this the right ordering of things to try?
148 if 'image/svg+xml' in data:
150 if 'image/svg+xml' in data:
149 self.log.debug("display: %s", msg.get('content', ''))
151 self.log.debug("display: %s", msg.get('content', ''))
150 svg = data['image/svg+xml']
152 svg = data['image/svg+xml']
151 self._append_svg(svg, True)
153 self._append_svg(svg, True)
152 elif 'image/png' in data:
154 elif 'image/png' in data:
153 self.log.debug("display: %s", msg.get('content', ''))
155 self.log.debug("display: %s", msg.get('content', ''))
154 # PNG data is base64 encoded as it passes over the network
156 # PNG data is base64 encoded as it passes over the network
155 # in a JSON structure so we decode it.
157 # in a JSON structure so we decode it.
156 png = decodestring(data['image/png'].encode('ascii'))
158 png = decodestring(data['image/png'].encode('ascii'))
157 self._append_png(png, True, metadata=metadata.get('image/png', None))
159 self._append_png(png, True, metadata=metadata.get('image/png', None))
158 elif 'image/jpeg' in data and self._jpg_supported:
160 elif 'image/jpeg' in data and self._jpg_supported:
159 self.log.debug("display: %s", msg.get('content', ''))
161 self.log.debug("display: %s", msg.get('content', ''))
160 jpg = decodestring(data['image/jpeg'].encode('ascii'))
162 jpg = decodestring(data['image/jpeg'].encode('ascii'))
161 self._append_jpg(jpg, True, metadata=metadata.get('image/jpeg', None))
163 self._append_jpg(jpg, True, metadata=metadata.get('image/jpeg', None))
162 else:
164 else:
163 # Default back to the plain text representation.
165 # Default back to the plain text representation.
164 return super(RichIPythonWidget, self)._handle_display_data(msg)
166 return super(RichIPythonWidget, self)._handle_display_data(msg)
165
167
166 #---------------------------------------------------------------------------
168 #---------------------------------------------------------------------------
167 # 'RichIPythonWidget' protected interface
169 # 'RichIPythonWidget' protected interface
168 #---------------------------------------------------------------------------
170 #---------------------------------------------------------------------------
169
171
170 def _append_jpg(self, jpg, before_prompt=False, metadata=None):
172 def _append_jpg(self, jpg, before_prompt=False, metadata=None):
171 """ Append raw JPG data to the widget."""
173 """ Append raw JPG data to the widget."""
172 self._append_custom(self._insert_jpg, jpg, before_prompt, metadata=metadata)
174 self._append_custom(self._insert_jpg, jpg, before_prompt, metadata=metadata)
173
175
174 def _append_png(self, png, before_prompt=False, metadata=None):
176 def _append_png(self, png, before_prompt=False, metadata=None):
175 """ Append raw PNG data to the widget.
177 """ Append raw PNG data to the widget.
176 """
178 """
177 self._append_custom(self._insert_png, png, before_prompt, metadata=metadata)
179 self._append_custom(self._insert_png, png, before_prompt, metadata=metadata)
178
180
179 def _append_svg(self, svg, before_prompt=False):
181 def _append_svg(self, svg, before_prompt=False):
180 """ Append raw SVG data to the widget.
182 """ Append raw SVG data to the widget.
181 """
183 """
182 self._append_custom(self._insert_svg, svg, before_prompt)
184 self._append_custom(self._insert_svg, svg, before_prompt)
183
185
184 def _add_image(self, image):
186 def _add_image(self, image):
185 """ Adds the specified QImage to the document and returns a
187 """ Adds the specified QImage to the document and returns a
186 QTextImageFormat that references it.
188 QTextImageFormat that references it.
187 """
189 """
188 document = self._control.document()
190 document = self._control.document()
189 name = str(image.cacheKey())
191 name = str(image.cacheKey())
190 document.addResource(QtGui.QTextDocument.ImageResource,
192 document.addResource(QtGui.QTextDocument.ImageResource,
191 QtCore.QUrl(name), image)
193 QtCore.QUrl(name), image)
192 format = QtGui.QTextImageFormat()
194 format = QtGui.QTextImageFormat()
193 format.setName(name)
195 format.setName(name)
194 return format
196 return format
195
197
196 def _copy_image(self, name):
198 def _copy_image(self, name):
197 """ Copies the ImageResource with 'name' to the clipboard.
199 """ Copies the ImageResource with 'name' to the clipboard.
198 """
200 """
199 image = self._get_image(name)
201 image = self._get_image(name)
200 QtGui.QApplication.clipboard().setImage(image)
202 QtGui.QApplication.clipboard().setImage(image)
201
203
202 def _get_image(self, name):
204 def _get_image(self, name):
203 """ Returns the QImage stored as the ImageResource with 'name'.
205 """ Returns the QImage stored as the ImageResource with 'name'.
204 """
206 """
205 document = self._control.document()
207 document = self._control.document()
206 image = document.resource(QtGui.QTextDocument.ImageResource,
208 image = document.resource(QtGui.QTextDocument.ImageResource,
207 QtCore.QUrl(name))
209 QtCore.QUrl(name))
208 return image
210 return image
209
211
210 def _get_image_tag(self, match, path = None, format = "png"):
212 def _get_image_tag(self, match, path = None, format = "png"):
211 """ Return (X)HTML mark-up for the image-tag given by match.
213 """ Return (X)HTML mark-up for the image-tag given by match.
212
214
213 Parameters
215 Parameters
214 ----------
216 ----------
215 match : re.SRE_Match
217 match : re.SRE_Match
216 A match to an HTML image tag as exported by Qt, with
218 A match to an HTML image tag as exported by Qt, with
217 match.group("Name") containing the matched image ID.
219 match.group("Name") containing the matched image ID.
218
220
219 path : string|None, optional [default None]
221 path : string|None, optional [default None]
220 If not None, specifies a path to which supporting files may be
222 If not None, specifies a path to which supporting files may be
221 written (e.g., for linked images). If None, all images are to be
223 written (e.g., for linked images). If None, all images are to be
222 included inline.
224 included inline.
223
225
224 format : "png"|"svg"|"jpg", optional [default "png"]
226 format : "png"|"svg"|"jpg", optional [default "png"]
225 Format for returned or referenced images.
227 Format for returned or referenced images.
226 """
228 """
227 if format in ("png","jpg"):
229 if format in ("png","jpg"):
228 try:
230 try:
229 image = self._get_image(match.group("name"))
231 image = self._get_image(match.group("name"))
230 except KeyError:
232 except KeyError:
231 return "<b>Couldn't find image %s</b>" % match.group("name")
233 return "<b>Couldn't find image %s</b>" % match.group("name")
232
234
233 if path is not None:
235 if path is not None:
234 if not os.path.exists(path):
236 if not os.path.exists(path):
235 os.mkdir(path)
237 os.mkdir(path)
236 relpath = os.path.basename(path)
238 relpath = os.path.basename(path)
237 if image.save("%s/qt_img%s.%s" % (path, match.group("name"), format),
239 if image.save("%s/qt_img%s.%s" % (path, match.group("name"), format),
238 "PNG"):
240 "PNG"):
239 return '<img src="%s/qt_img%s.%s">' % (relpath,
241 return '<img src="%s/qt_img%s.%s">' % (relpath,
240 match.group("name"),format)
242 match.group("name"),format)
241 else:
243 else:
242 return "<b>Couldn't save image!</b>"
244 return "<b>Couldn't save image!</b>"
243 else:
245 else:
244 ba = QtCore.QByteArray()
246 ba = QtCore.QByteArray()
245 buffer_ = QtCore.QBuffer(ba)
247 buffer_ = QtCore.QBuffer(ba)
246 buffer_.open(QtCore.QIODevice.WriteOnly)
248 buffer_.open(QtCore.QIODevice.WriteOnly)
247 image.save(buffer_, format.upper())
249 image.save(buffer_, format.upper())
248 buffer_.close()
250 buffer_.close()
249 return '<img src="data:image/%s;base64,\n%s\n" />' % (
251 return '<img src="data:image/%s;base64,\n%s\n" />' % (
250 format,re.sub(r'(.{60})',r'\1\n',str(ba.toBase64())))
252 format,re.sub(r'(.{60})',r'\1\n',str(ba.toBase64())))
251
253
252 elif format == "svg":
254 elif format == "svg":
253 try:
255 try:
254 svg = str(self._name_to_svg_map[match.group("name")])
256 svg = str(self._name_to_svg_map[match.group("name")])
255 except KeyError:
257 except KeyError:
256 if not self._svg_warning_displayed:
258 if not self._svg_warning_displayed:
257 QtGui.QMessageBox.warning(self, 'Error converting PNG to SVG.',
259 QtGui.QMessageBox.warning(self, 'Error converting PNG to SVG.',
258 'Cannot convert PNG images to SVG, export with PNG figures instead. '
260 'Cannot convert PNG images to SVG, export with PNG figures instead. '
259 'If you want to export matplotlib figures as SVG, add '
261 'If you want to export matplotlib figures as SVG, add '
260 'to your ipython config:\n\n'
262 'to your ipython config:\n\n'
261 '\tc.InlineBackend.figure_format = \'svg\'\n\n'
263 '\tc.InlineBackend.figure_format = \'svg\'\n\n'
262 'And regenerate the figures.',
264 'And regenerate the figures.',
263 QtGui.QMessageBox.Ok)
265 QtGui.QMessageBox.Ok)
264 self._svg_warning_displayed = True
266 self._svg_warning_displayed = True
265 return ("<b>Cannot convert PNG images to SVG.</b> "
267 return ("<b>Cannot convert PNG images to SVG.</b> "
266 "You must export this session with PNG images. "
268 "You must export this session with PNG images. "
267 "If you want to export matplotlib figures as SVG, add to your config "
269 "If you want to export matplotlib figures as SVG, add to your config "
268 "<span>c.InlineBackend.figure_format = 'svg'</span> "
270 "<span>c.InlineBackend.figure_format = 'svg'</span> "
269 "and regenerate the figures.")
271 "and regenerate the figures.")
270
272
271 # Not currently checking path, because it's tricky to find a
273 # Not currently checking path, because it's tricky to find a
272 # cross-browser way to embed external SVG images (e.g., via
274 # cross-browser way to embed external SVG images (e.g., via
273 # object or embed tags).
275 # object or embed tags).
274
276
275 # Chop stand-alone header from matplotlib SVG
277 # Chop stand-alone header from matplotlib SVG
276 offset = svg.find("<svg")
278 offset = svg.find("<svg")
277 assert(offset > -1)
279 assert(offset > -1)
278
280
279 return svg[offset:]
281 return svg[offset:]
280
282
281 else:
283 else:
282 return '<b>Unrecognized image format</b>'
284 return '<b>Unrecognized image format</b>'
283
285
284 def _insert_jpg(self, cursor, jpg, metadata=None):
286 def _insert_jpg(self, cursor, jpg, metadata=None):
285 """ Insert raw PNG data into the widget."""
287 """ Insert raw PNG data into the widget."""
286 self._insert_img(cursor, jpg, 'jpg', metadata=metadata)
288 self._insert_img(cursor, jpg, 'jpg', metadata=metadata)
287
289
288 def _insert_png(self, cursor, png, metadata=None):
290 def _insert_png(self, cursor, png, metadata=None):
289 """ Insert raw PNG data into the widget.
291 """ Insert raw PNG data into the widget.
290 """
292 """
291 self._insert_img(cursor, png, 'png', metadata=metadata)
293 self._insert_img(cursor, png, 'png', metadata=metadata)
292
294
293 def _insert_img(self, cursor, img, fmt, metadata=None):
295 def _insert_img(self, cursor, img, fmt, metadata=None):
294 """ insert a raw image, jpg or png """
296 """ insert a raw image, jpg or png """
295 if metadata:
297 if metadata:
296 width = metadata.get('width', None)
298 width = metadata.get('width', None)
297 height = metadata.get('height', None)
299 height = metadata.get('height', None)
298 else:
300 else:
299 width = height = None
301 width = height = None
300 try:
302 try:
301 image = QtGui.QImage()
303 image = QtGui.QImage()
302 image.loadFromData(img, fmt.upper())
304 image.loadFromData(img, fmt.upper())
303 if width and height:
305 if width and height:
304 image = image.scaled(width, height, transformMode=QtCore.Qt.SmoothTransformation)
306 image = image.scaled(width, height, transformMode=QtCore.Qt.SmoothTransformation)
305 elif width and not height:
307 elif width and not height:
306 image = image.scaledToWidth(width, transformMode=QtCore.Qt.SmoothTransformation)
308 image = image.scaledToWidth(width, transformMode=QtCore.Qt.SmoothTransformation)
307 elif height and not width:
309 elif height and not width:
308 image = image.scaledToHeight(height, transformMode=QtCore.Qt.SmoothTransformation)
310 image = image.scaledToHeight(height, transformMode=QtCore.Qt.SmoothTransformation)
309 except ValueError:
311 except ValueError:
310 self._insert_plain_text(cursor, 'Received invalid %s data.'%fmt)
312 self._insert_plain_text(cursor, 'Received invalid %s data.'%fmt)
311 else:
313 else:
312 format = self._add_image(image)
314 format = self._add_image(image)
313 cursor.insertBlock()
315 cursor.insertBlock()
314 cursor.insertImage(format)
316 cursor.insertImage(format)
315 cursor.insertBlock()
317 cursor.insertBlock()
316
318
317 def _insert_svg(self, cursor, svg):
319 def _insert_svg(self, cursor, svg):
318 """ Insert raw SVG data into the widet.
320 """ Insert raw SVG data into the widet.
319 """
321 """
320 try:
322 try:
321 image = svg_to_image(svg)
323 image = svg_to_image(svg)
322 except ValueError:
324 except ValueError:
323 self._insert_plain_text(cursor, 'Received invalid SVG data.')
325 self._insert_plain_text(cursor, 'Received invalid SVG data.')
324 else:
326 else:
325 format = self._add_image(image)
327 format = self._add_image(image)
326 self._name_to_svg_map[format.name()] = svg
328 self._name_to_svg_map[format.name()] = svg
327 cursor.insertBlock()
329 cursor.insertBlock()
328 cursor.insertImage(format)
330 cursor.insertImage(format)
329 cursor.insertBlock()
331 cursor.insertBlock()
330
332
331 def _save_image(self, name, format='PNG'):
333 def _save_image(self, name, format='PNG'):
332 """ Shows a save dialog for the ImageResource with 'name'.
334 """ Shows a save dialog for the ImageResource with 'name'.
333 """
335 """
334 dialog = QtGui.QFileDialog(self._control, 'Save Image')
336 dialog = QtGui.QFileDialog(self._control, 'Save Image')
335 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
337 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
336 dialog.setDefaultSuffix(format.lower())
338 dialog.setDefaultSuffix(format.lower())
337 dialog.setNameFilter('%s file (*.%s)' % (format, format.lower()))
339 dialog.setNameFilter('%s file (*.%s)' % (format, format.lower()))
338 if dialog.exec_():
340 if dialog.exec_():
339 filename = dialog.selectedFiles()[0]
341 filename = dialog.selectedFiles()[0]
340 image = self._get_image(name)
342 image = self._get_image(name)
341 image.save(filename, format)
343 image.save(filename, format)
General Comments 0
You need to be logged in to leave comments. Login now