##// END OF EJS Templates
* Moved KernelManager attribute management code in FrontendWidget into a mixin class usable in any Qt frontend. Registering handlers for message types is now trivial....
epatters -
Show More
@@ -0,0 +1,85 b''
1 """ Defines a convenient mix-in class for implementing Qt frontends.
2 """
3
4 class BaseFrontendMixin(object):
5 """ A mix-in class for implementing Qt frontends.
6
7 To handle messages of a particular type, frontends need only define an
8 appropriate handler method. For example, to handle 'stream' messaged, define
9 a '_handle_stream(msg)' method.
10 """
11
12 #---------------------------------------------------------------------------
13 # 'BaseFrontendMixin' concrete interface
14 #---------------------------------------------------------------------------
15
16 def _get_kernel_manager(self):
17 """ Returns the current kernel manager.
18 """
19 return self._kernel_manager
20
21 def _set_kernel_manager(self, kernel_manager):
22 """ Disconnect from the current kernel manager (if any) and set a new
23 kernel manager.
24 """
25 # Disconnect the old kernel manager, if necessary.
26 old_manager = self._kernel_manager
27 if old_manager is not None:
28 old_manager.started_channels.disconnect(self._started_channels)
29 old_manager.stopped_channels.disconnect(self._stopped_channels)
30
31 # Disconnect the old kernel manager's channels.
32 old_manager.sub_channel.message_received.disconnect(self._dispatch)
33 old_manager.xreq_channel.message_received.disconnect(self._dispatch)
34 old_manager.rep_channel.message_received.connect(self._dispatch)
35
36 # Handle the case where the old kernel manager is still listening.
37 if old_manager.channels_running:
38 self._stopped_channels()
39
40 # Set the new kernel manager.
41 self._kernel_manager = kernel_manager
42 if kernel_manager is None:
43 return
44
45 # Connect the new kernel manager.
46 kernel_manager.started_channels.connect(self._started_channels)
47 kernel_manager.stopped_channels.connect(self._stopped_channels)
48
49 # Connect the new kernel manager's channels.
50 kernel_manager.sub_channel.message_received.connect(self._dispatch)
51 kernel_manager.xreq_channel.message_received.connect(self._dispatch)
52 kernel_manager.rep_channel.message_received.connect(self._dispatch)
53
54 # Handle the case where the kernel manager started channels before
55 # we connected.
56 if kernel_manager.channels_running:
57 self._started_channels()
58
59 kernel_manager = property(_get_kernel_manager, _set_kernel_manager)
60
61 #---------------------------------------------------------------------------
62 # 'BaseFrontendMixin' abstract interface
63 #---------------------------------------------------------------------------
64
65 def _started_channels(self):
66 """ Called when the KernelManager channels have started listening or
67 when the frontend is assigned an already listening KernelManager.
68 """
69
70 def _stopped_channels(self):
71 """ Called when the KernelManager channels have stopped listening or
72 when a listening KernelManager is removed from the frontend.
73 """
74
75 #---------------------------------------------------------------------------
76 # Private interface
77 #---------------------------------------------------------------------------
78
79 def _dispatch(self, msg):
80 """ Call the frontend handler associated with
81 """
82 msg_type = msg['msg_type']
83 handler = getattr(self, '_handle_' + msg_type, None)
84 if handler:
85 handler(msg)
@@ -1,376 +1,340 b''
1 1 # Standard library imports
2 2 import signal
3 3 import sys
4 4
5 5 # System library imports
6 6 from pygments.lexers import PythonLexer
7 7 from PyQt4 import QtCore, QtGui
8 8 import zmq
9 9
10 10 # Local imports
11 11 from IPython.core.inputsplitter import InputSplitter
12 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
12 13 from call_tip_widget import CallTipWidget
13 14 from completion_lexer import CompletionLexer
14 15 from console_widget import HistoryConsoleWidget
15 16 from pygments_highlighter import PygmentsHighlighter
16 17
17 18
18 19 class FrontendHighlighter(PygmentsHighlighter):
19 20 """ A PygmentsHighlighter that can be turned on and off and that ignores
20 21 prompts.
21 22 """
22 23
23 24 def __init__(self, frontend):
24 25 super(FrontendHighlighter, self).__init__(frontend._control.document())
25 26 self._current_offset = 0
26 27 self._frontend = frontend
27 28 self.highlighting_on = False
28 29
29 30 def highlightBlock(self, qstring):
30 31 """ Highlight a block of text. Reimplemented to highlight selectively.
31 32 """
32 33 if not self.highlighting_on:
33 34 return
34 35
35 36 # The input to this function is unicode string that may contain
36 37 # paragraph break characters, non-breaking spaces, etc. Here we acquire
37 38 # the string as plain text so we can compare it.
38 39 current_block = self.currentBlock()
39 40 string = self._frontend._get_block_plain_text(current_block)
40 41
41 42 # Decide whether to check for the regular or continuation prompt.
42 43 if current_block.contains(self._frontend._prompt_pos):
43 44 prompt = self._frontend._prompt
44 45 else:
45 46 prompt = self._frontend._continuation_prompt
46 47
47 48 # Don't highlight the part of the string that contains the prompt.
48 49 if string.startswith(prompt):
49 50 self._current_offset = len(prompt)
50 51 qstring.remove(0, len(prompt))
51 52 else:
52 53 self._current_offset = 0
53 54
54 55 PygmentsHighlighter.highlightBlock(self, qstring)
55 56
56 57 def setFormat(self, start, count, format):
57 58 """ Reimplemented to highlight selectively.
58 59 """
59 60 start += self._current_offset
60 61 PygmentsHighlighter.setFormat(self, start, count, format)
61 62
62 63
63 class FrontendWidget(HistoryConsoleWidget):
64 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
64 65 """ A Qt frontend for a generic Python kernel.
65 66 """
66 67
67 68 # Emitted when an 'execute_reply' is received from the kernel.
68 69 executed = QtCore.pyqtSignal(object)
69 70
70 71 #---------------------------------------------------------------------------
71 72 # 'object' interface
72 73 #---------------------------------------------------------------------------
73 74
74 75 def __init__(self, *args, **kw):
75 76 super(FrontendWidget, self).__init__(*args, **kw)
76 77
77 78 # FrontendWidget protected variables.
78 79 self._call_tip_widget = CallTipWidget(self._control)
79 80 self._completion_lexer = CompletionLexer(PythonLexer())
80 81 self._hidden = True
81 82 self._highlighter = FrontendHighlighter(self)
82 83 self._input_splitter = InputSplitter(input_mode='replace')
83 84 self._kernel_manager = None
84 85
85 86 # Configure the ConsoleWidget.
86 87 self.tab_width = 4
87 88 self._set_continuation_prompt('... ')
88 89
89 90 # Connect signal handlers.
90 91 document = self._control.document()
91 92 document.contentsChange.connect(self._document_contents_change)
92 93
93 94 #---------------------------------------------------------------------------
94 95 # 'ConsoleWidget' abstract interface
95 96 #---------------------------------------------------------------------------
96 97
97 98 def _is_complete(self, source, interactive):
98 99 """ Returns whether 'source' can be completely processed and a new
99 100 prompt created. When triggered by an Enter/Return key press,
100 101 'interactive' is True; otherwise, it is False.
101 102 """
102 103 complete = self._input_splitter.push(source.expandtabs(4))
103 104 if interactive:
104 105 complete = not self._input_splitter.push_accepts_more()
105 106 return complete
106 107
107 108 def _execute(self, source, hidden):
108 109 """ Execute 'source'. If 'hidden', do not show any output.
109 110 """
110 111 self.kernel_manager.xreq_channel.execute(source)
111 112 self._hidden = hidden
112 113
113 114 def _execute_interrupt(self):
114 115 """ Attempts to stop execution. Returns whether this method has an
115 116 implementation.
116 117 """
117 118 self._interrupt_kernel()
118 119 return True
119 120
120 121 def _prompt_started_hook(self):
121 122 """ Called immediately after a new prompt is displayed.
122 123 """
123 124 if not self._reading:
124 125 self._highlighter.highlighting_on = True
125 126
126 127 # Auto-indent if this is a continuation prompt.
127 128 if self._get_prompt_cursor().blockNumber() != \
128 129 self._get_end_cursor().blockNumber():
129 130 spaces = self._input_splitter.indent_spaces
130 131 self._append_plain_text('\t' * (spaces / self.tab_width))
131 132 self._append_plain_text(' ' * (spaces % self.tab_width))
132 133
133 134 def _prompt_finished_hook(self):
134 135 """ Called immediately after a prompt is finished, i.e. when some input
135 136 will be processed and a new prompt displayed.
136 137 """
137 138 if not self._reading:
138 139 self._highlighter.highlighting_on = False
139 140
140 141 def _tab_pressed(self):
141 142 """ Called when the tab key is pressed. Returns whether to continue
142 143 processing the event.
143 144 """
144 145 self._keep_cursor_in_buffer()
145 146 cursor = self._get_cursor()
146 147 return not self._complete()
147 148
148 149 #---------------------------------------------------------------------------
149 # 'FrontendWidget' interface
150 # 'BaseFrontendMixin' abstract interface
150 151 #---------------------------------------------------------------------------
151 152
152 def execute_file(self, path, hidden=False):
153 """ Attempts to execute file with 'path'. If 'hidden', no output is
154 shown.
153 def _handle_complete_reply(self, rep):
154 """ Handle replies for tab completion.
155 155 """
156 self.execute('execfile("%s")' % path, hidden=hidden)
156 cursor = self._get_cursor()
157 if rep['parent_header']['msg_id'] == self._complete_id and \
158 cursor.position() == self._complete_pos:
159 text = '.'.join(self._get_context())
160 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
161 self._complete_with_items(cursor, rep['content']['matches'])
157 162
158 def _get_kernel_manager(self):
159 """ Returns the current kernel manager.
163 def _handle_execute_reply(self, msg):
164 """ Handles replies for code execution.
160 165 """
161 return self._kernel_manager
166 if not self._hidden:
167 # Make sure that all output from the SUB channel has been processed
168 # before writing a new prompt.
169 self.kernel_manager.sub_channel.flush()
170
171 content = msg['content']
172 status = content['status']
173 if status == 'ok':
174 self._process_execute_ok(msg)
175 elif status == 'error':
176 self._process_execute_error(msg)
177 elif status == 'abort':
178 self._process_execute_abort(msg)
179
180 self._hidden = True
181 self._show_interpreter_prompt()
182 self.executed.emit(msg)
183
184 def _handle_input_request(self, msg):
185 """ Handle requests for raw_input.
186 """
187 # Make sure that all output from the SUB channel has been processed
188 # before entering readline mode.
189 self.kernel_manager.sub_channel.flush()
190
191 def callback(line):
192 self.kernel_manager.rep_channel.input(line)
193 self._readline(msg['content']['prompt'], callback=callback)
162 194
163 def _set_kernel_manager(self, kernel_manager):
164 """ Disconnect from the current kernel manager (if any) and set a new
165 kernel manager.
195 def _handle_object_info_reply(self, rep):
196 """ Handle replies for call tips.
166 197 """
167 # Disconnect the old kernel manager, if necessary.
168 if self._kernel_manager is not None:
169 self._kernel_manager.started_channels.disconnect(
170 self._started_channels)
171 self._kernel_manager.stopped_channels.disconnect(
172 self._stopped_channels)
173
174 # Disconnect the old kernel manager's channels.
175 sub = self._kernel_manager.sub_channel
176 xreq = self._kernel_manager.xreq_channel
177 rep = self._kernel_manager.rep_channel
178 sub.message_received.disconnect(self._handle_sub)
179 xreq.execute_reply.disconnect(self._handle_execute_reply)
180 xreq.complete_reply.disconnect(self._handle_complete_reply)
181 xreq.object_info_reply.disconnect(self._handle_object_info_reply)
182 rep.input_requested.disconnect(self._handle_req)
183
184 # Handle the case where the old kernel manager is still listening.
185 if self._kernel_manager.channels_running:
186 self._stopped_channels()
187
188 # Set the new kernel manager.
189 self._kernel_manager = kernel_manager
190 if kernel_manager is None:
191 return
198 cursor = self._get_cursor()
199 if rep['parent_header']['msg_id'] == self._call_tip_id and \
200 cursor.position() == self._call_tip_pos:
201 doc = rep['content']['docstring']
202 if doc:
203 self._call_tip_widget.show_docstring(doc)
192 204
193 # Connect the new kernel manager.
194 kernel_manager.started_channels.connect(self._started_channels)
195 kernel_manager.stopped_channels.connect(self._stopped_channels)
196
197 # Connect the new kernel manager's channels.
198 sub = kernel_manager.sub_channel
199 xreq = kernel_manager.xreq_channel
200 rep = kernel_manager.rep_channel
201 sub.message_received.connect(self._handle_sub)
202 xreq.execute_reply.connect(self._handle_execute_reply)
203 xreq.complete_reply.connect(self._handle_complete_reply)
204 xreq.object_info_reply.connect(self._handle_object_info_reply)
205 rep.input_requested.connect(self._handle_req)
206
207 # Handle the case where the kernel manager started channels before
208 # we connected.
209 if kernel_manager.channels_running:
210 self._started_channels()
205 def _handle_pyout(self, msg):
206 """ Handle display hook output.
207 """
208 self._append_plain_text(msg['content']['data'] + '\n')
211 209
212 kernel_manager = property(_get_kernel_manager, _set_kernel_manager)
210 def _handle_stream(self, msg):
211 """ Handle stdout, stderr, and stdin.
212 """
213 self._append_plain_text(msg['content']['data'])
214 self._control.moveCursor(QtGui.QTextCursor.End)
215
216 def _started_channels(self):
217 """ Called when the KernelManager channels have started listening or
218 when the frontend is assigned an already listening KernelManager.
219 """
220 self._reset()
221 self._append_plain_text(self._get_banner())
222 self._show_interpreter_prompt()
223
224 def _stopped_channels(self):
225 """ Called when the KernelManager channels have stopped listening or
226 when a listening KernelManager is removed from the frontend.
227 """
228 # FIXME: Print a message here?
229 pass
230
231 #---------------------------------------------------------------------------
232 # 'FrontendWidget' interface
233 #---------------------------------------------------------------------------
234
235 def execute_file(self, path, hidden=False):
236 """ Attempts to execute file with 'path'. If 'hidden', no output is
237 shown.
238 """
239 self.execute('execfile("%s")' % path, hidden=hidden)
213 240
214 241 #---------------------------------------------------------------------------
215 242 # 'FrontendWidget' protected interface
216 243 #---------------------------------------------------------------------------
217 244
218 245 def _call_tip(self):
219 246 """ Shows a call tip, if appropriate, at the current cursor location.
220 247 """
221 248 # Decide if it makes sense to show a call tip
222 249 cursor = self._get_cursor()
223 250 cursor.movePosition(QtGui.QTextCursor.Left)
224 251 document = self._control.document()
225 252 if document.characterAt(cursor.position()).toAscii() != '(':
226 253 return False
227 254 context = self._get_context(cursor)
228 255 if not context:
229 256 return False
230 257
231 258 # Send the metadata request to the kernel
232 259 name = '.'.join(context)
233 260 self._call_tip_id = self.kernel_manager.xreq_channel.object_info(name)
234 261 self._call_tip_pos = self._get_cursor().position()
235 262 return True
236 263
237 264 def _complete(self):
238 265 """ Performs completion at the current cursor location.
239 266 """
240 267 # Decide if it makes sense to do completion
241 268 context = self._get_context()
242 269 if not context:
243 270 return False
244 271
245 272 # Send the completion request to the kernel
246 273 text = '.'.join(context)
247 274 self._complete_id = self.kernel_manager.xreq_channel.complete(
248 275 text, self._get_input_buffer_cursor_line(), self.input_buffer)
249 276 self._complete_pos = self._get_cursor().position()
250 277 return True
251 278
252 279 def _get_banner(self):
253 280 """ Gets a banner to display at the beginning of a session.
254 281 """
255 282 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
256 283 '"license" for more information.'
257 284 return banner % (sys.version, sys.platform)
258 285
259 286 def _get_context(self, cursor=None):
260 287 """ Gets the context at the current cursor location.
261 288 """
262 289 if cursor is None:
263 290 cursor = self._get_cursor()
264 291 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
265 292 QtGui.QTextCursor.KeepAnchor)
266 293 text = str(cursor.selection().toPlainText())
267 294 return self._completion_lexer.get_context(text)
268 295
269 296 def _interrupt_kernel(self):
270 297 """ Attempts to the interrupt the kernel.
271 298 """
272 299 if self.kernel_manager.has_kernel:
273 300 self.kernel_manager.signal_kernel(signal.SIGINT)
274 301 else:
275 302 self._append_plain_text('Kernel process is either remote or '
276 303 'unspecified. Cannot interrupt.\n')
277 304
278 def _show_interpreter_prompt(self):
279 """ Shows a prompt for the interpreter.
305 def _process_execute_abort(self, msg):
306 """ Process a reply for an aborted execution request.
280 307 """
281 self._show_prompt('>>> ')
282
283 #------ Signal handlers ----------------------------------------------------
308 self._append_plain_text("ERROR: execution aborted\n")
284 309
285 def _started_channels(self):
286 """ Called when the kernel manager has started listening.
310 def _process_execute_error(self, msg):
311 """ Process a reply for an execution request that resulted in an error.
287 312 """
288 self._reset()
289 self._append_plain_text(self._get_banner())
290 self._show_interpreter_prompt()
313 content = msg['content']
314 traceback = ''.join(content['traceback'])
315 self._append_plain_text(traceback)
291 316
292 def _stopped_channels(self):
293 """ Called when the kernel manager has stopped listening.
317 def _process_execute_ok(self, msg):
318 """ Process a reply for a successful execution equest.
294 319 """
295 # FIXME: Print a message here?
320 # The basic FrontendWidget doesn't handle payloads, as they are a
321 # mechanism for going beyond the standard Python interpreter model.
296 322 pass
297 323
324 def _show_interpreter_prompt(self):
325 """ Shows a prompt for the interpreter.
326 """
327 self._show_prompt('>>> ')
328
329 #------ Signal handlers ----------------------------------------------------
330
298 331 def _document_contents_change(self, position, removed, added):
299 332 """ Called whenever the document's content changes. Display a call tip
300 333 if appropriate.
301 334 """
302 335 # Calculate where the cursor should be *after* the change:
303 336 position += added
304 337
305 338 document = self._control.document()
306 339 if position == self._get_cursor().position():
307 340 self._call_tip()
308
309 def _handle_req(self, req):
310 # Make sure that all output from the SUB channel has been processed
311 # before entering readline mode.
312 self.kernel_manager.sub_channel.flush()
313
314 def callback(line):
315 self.kernel_manager.rep_channel.input(line)
316 self._readline(req['content']['prompt'], callback=callback)
317
318 def _handle_sub(self, omsg):
319 if self._hidden:
320 return
321 handler = getattr(self, '_handle_%s' % omsg['msg_type'], None)
322 if handler is not None:
323 handler(omsg)
324
325 def _handle_pyout(self, omsg):
326 self._append_plain_text(omsg['content']['data'] + '\n')
327
328 def _handle_stream(self, omsg):
329 self._append_plain_text(omsg['content']['data'])
330 self._control.moveCursor(QtGui.QTextCursor.End)
331
332 def _handle_execute_reply(self, reply):
333 if self._hidden:
334 return
335
336 # Make sure that all output from the SUB channel has been processed
337 # before writing a new prompt.
338 self.kernel_manager.sub_channel.flush()
339
340 content = reply['content']
341 status = content['status']
342 if status == 'ok':
343 self._handle_execute_payload(content['payload'])
344 elif status == 'error':
345 self._handle_execute_error(reply)
346 elif status == 'aborted':
347 text = "ERROR: ABORTED\n"
348 self._append_plain_text(text)
349
350 self._hidden = True
351 self._show_interpreter_prompt()
352 self.executed.emit(reply)
353
354 def _handle_execute_error(self, reply):
355 content = reply['content']
356 traceback = ''.join(content['traceback'])
357 self._append_plain_text(traceback)
358
359 def _handle_execute_payload(self, payload):
360 pass
361
362 def _handle_complete_reply(self, rep):
363 cursor = self._get_cursor()
364 if rep['parent_header']['msg_id'] == self._complete_id and \
365 cursor.position() == self._complete_pos:
366 text = '.'.join(self._get_context())
367 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
368 self._complete_with_items(cursor, rep['content']['matches'])
369
370 def _handle_object_info_reply(self, rep):
371 cursor = self._get_cursor()
372 if rep['parent_header']['msg_id'] == self._call_tip_id and \
373 cursor.position() == self._call_tip_pos:
374 doc = rep['content']['docstring']
375 if doc:
376 self._call_tip_widget.show_docstring(doc)
@@ -1,184 +1,186 b''
1 1 # System library imports
2 2 from PyQt4 import QtCore, QtGui
3 3
4 4 # Local imports
5 5 from IPython.core.usage import default_banner
6 6 from frontend_widget import FrontendWidget
7 7
8 8
9 9 class IPythonWidget(FrontendWidget):
10 10 """ A FrontendWidget for an IPython kernel.
11 11 """
12 12
13 13 # The default stylesheet: black text on a white background.
14 14 default_stylesheet = """
15 15 .error { color: red; }
16 16 .in-prompt { color: navy; }
17 17 .in-prompt-number { font-weight: bold; }
18 18 .out-prompt { color: darkred; }
19 19 .out-prompt-number { font-weight: bold; }
20 20 """
21 21
22 22 # A dark stylesheet: white text on a black background.
23 23 dark_stylesheet = """
24 24 QPlainTextEdit { background-color: black; color: white }
25 25 QFrame { border: 1px solid grey; }
26 26 .error { color: red; }
27 27 .in-prompt { color: lime; }
28 28 .in-prompt-number { color: lime; font-weight: bold; }
29 29 .out-prompt { color: red; }
30 30 .out-prompt-number { color: red; font-weight: bold; }
31 31 """
32 32
33 33 # Default prompts.
34 34 in_prompt = '<br/>In [<span class="in-prompt-number">%i</span>]: '
35 35 out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
36 36
37 37 #---------------------------------------------------------------------------
38 38 # 'object' interface
39 39 #---------------------------------------------------------------------------
40 40
41 41 def __init__(self, *args, **kw):
42 42 super(IPythonWidget, self).__init__(*args, **kw)
43 43
44 44 # Initialize protected variables.
45 45 self._previous_prompt_blocks = []
46 46 self._prompt_count = 0
47 47
48 48 # Set a default stylesheet.
49 49 self.reset_styling()
50 50
51 51 #---------------------------------------------------------------------------
52 # 'BaseFrontendMixin' abstract interface
53 #---------------------------------------------------------------------------
54
55 def _handle_pyout(self, msg):
56 """ Reimplemented for IPython-style "display hook".
57 """
58 self._append_html(self._make_out_prompt(self._prompt_count))
59 self._save_prompt_block()
60
61 self._append_plain_text(msg['content']['data'] + '\n')
62
63 #---------------------------------------------------------------------------
52 64 # 'FrontendWidget' interface
53 65 #---------------------------------------------------------------------------
54 66
55 67 def execute_file(self, path, hidden=False):
56 68 """ Reimplemented to use the 'run' magic.
57 69 """
58 70 self.execute('run %s' % path, hidden=hidden)
59 71
60 72 #---------------------------------------------------------------------------
61 73 # 'FrontendWidget' protected interface
62 74 #---------------------------------------------------------------------------
63 75
64 76 def _get_banner(self):
65 77 """ Reimplemented to return IPython's default banner.
66 78 """
67 79 return default_banner
68 80
81 def _process_execute_error(self, msg):
82 """ Reimplemented for IPython-style traceback formatting.
83 """
84 content = msg['content']
85 traceback_lines = content['traceback'][:]
86 traceback = ''.join(traceback_lines)
87 traceback = traceback.replace(' ', '&nbsp;')
88 traceback = traceback.replace('\n', '<br/>')
89
90 ename = content['ename']
91 ename_styled = '<span class="error">%s</span>' % ename
92 traceback = traceback.replace(ename, ename_styled)
93
94 self._append_html(traceback)
95
69 96 def _show_interpreter_prompt(self):
70 97 """ Reimplemented for IPython-style prompts.
71 98 """
72 99 # Update old prompt numbers if necessary.
73 100 previous_prompt_number = self._prompt_count
74 101 if previous_prompt_number != self._prompt_count:
75 102 for i, (block, length) in enumerate(self._previous_prompt_blocks):
76 103 if block.isValid():
77 104 cursor = QtGui.QTextCursor(block)
78 105 cursor.movePosition(QtGui.QTextCursor.Right,
79 106 QtGui.QTextCursor.KeepAnchor, length-1)
80 107 if i == 0:
81 108 prompt = self._make_in_prompt(previous_prompt_number)
82 109 else:
83 110 prompt = self._make_out_prompt(previous_prompt_number)
84 111 self._insert_html(cursor, prompt)
85 112 self._previous_prompt_blocks = []
86 113
87 114 # Show a new prompt.
88 115 self._prompt_count += 1
89 116 self._show_prompt(self._make_in_prompt(self._prompt_count), html=True)
90 117 self._save_prompt_block()
91 118
92 119 # Update continuation prompt to reflect (possibly) new prompt length.
93 120 self._set_continuation_prompt(
94 121 self._make_continuation_prompt(self._prompt), html=True)
95 122
96 #------ Signal handlers ----------------------------------------------------
97
98 def _handle_execute_error(self, reply):
99 """ Reimplemented for IPython-style traceback formatting.
100 """
101 content = reply['content']
102 traceback_lines = content['traceback'][:]
103 traceback = ''.join(traceback_lines)
104 traceback = traceback.replace(' ', '&nbsp;')
105 traceback = traceback.replace('\n', '<br/>')
106
107 ename = content['ename']
108 ename_styled = '<span class="error">%s</span>' % ename
109 traceback = traceback.replace(ename, ename_styled)
110
111 self._append_html(traceback)
112
113 def _handle_pyout(self, omsg):
114 """ Reimplemented for IPython-style "display hook".
115 """
116 self._append_html(self._make_out_prompt(self._prompt_count))
117 self._save_prompt_block()
118
119 self._append_plain_text(omsg['content']['data'] + '\n')
120
121 123 #---------------------------------------------------------------------------
122 124 # 'IPythonWidget' interface
123 125 #---------------------------------------------------------------------------
124 126
125 127 def reset_styling(self):
126 128 """ Restores the default IPythonWidget styling.
127 129 """
128 130 self.set_styling(self.default_stylesheet, syntax_style='default')
129 131 #self.set_styling(self.dark_stylesheet, syntax_style='monokai')
130 132
131 133 def set_styling(self, stylesheet, syntax_style=None):
132 134 """ Sets the IPythonWidget styling.
133 135
134 136 Parameters:
135 137 -----------
136 138 stylesheet : str
137 139 A CSS stylesheet. The stylesheet can contain classes for:
138 140 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
139 141 2. Pygments: .c, .k, .o, etc (see PygmentsHighlighter)
140 142 3. IPython: .error, .in-prompt, .out-prompt, etc.
141 143
142 144 syntax_style : str or None [default None]
143 145 If specified, use the Pygments style with given name. Otherwise,
144 146 the stylesheet is queried for Pygments style information.
145 147 """
146 148 self.setStyleSheet(stylesheet)
147 149 self._control.document().setDefaultStyleSheet(stylesheet)
148 150
149 151 if syntax_style is None:
150 152 self._highlighter.set_style_sheet(stylesheet)
151 153 else:
152 154 self._highlighter.set_style(syntax_style)
153 155
154 156 #---------------------------------------------------------------------------
155 157 # 'IPythonWidget' protected interface
156 158 #---------------------------------------------------------------------------
157 159
158 160 def _make_in_prompt(self, number):
159 161 """ Given a prompt number, returns an HTML In prompt.
160 162 """
161 163 body = self.in_prompt % number
162 164 return '<span class="in-prompt">%s</span>' % body
163 165
164 166 def _make_continuation_prompt(self, prompt):
165 167 """ Given a plain text version of an In prompt, returns an HTML
166 168 continuation prompt.
167 169 """
168 170 end_chars = '...: '
169 171 space_count = len(prompt.lstrip('\n')) - len(end_chars)
170 172 body = '&nbsp;' * space_count + end_chars
171 173 return '<span class="in-prompt">%s</span>' % body
172 174
173 175 def _make_out_prompt(self, number):
174 176 """ Given a prompt number, returns an HTML Out prompt.
175 177 """
176 178 body = self.out_prompt % number
177 179 return '<span class="out-prompt">%s</span>' % body
178 180
179 181 def _save_prompt_block(self):
180 182 """ Assuming a prompt has just been written at the end of the buffer,
181 183 store the QTextBlock that contains it and its length.
182 184 """
183 185 block = self._control.document().lastBlock()
184 186 self._previous_prompt_blocks.append((block, block.length()))
@@ -1,118 +1,119 b''
1 1 # System library imports
2 2 from PyQt4 import QtCore, QtGui
3 3
4 4 # Local imports
5 5 from IPython.frontend.qt.svg import save_svg, svg_to_clipboard, svg_to_image
6 6 from ipython_widget import IPythonWidget
7 7
8 8
9 9 class RichIPythonWidget(IPythonWidget):
10 10 """ An IPythonWidget that supports rich text, including lists, images, and
11 11 tables. Note that raw performance will be reduced compared to the plain
12 12 text version.
13 13 """
14 14
15 15 # Protected class variables.
16 16 _svg_text_format_property = 1
17 17
18 18 #---------------------------------------------------------------------------
19 19 # 'QObject' interface
20 20 #---------------------------------------------------------------------------
21 21
22 22 def __init__(self, parent=None):
23 23 """ Create a RichIPythonWidget.
24 24 """
25 25 super(RichIPythonWidget, self).__init__(kind='rich', parent=parent)
26 26
27 27 #---------------------------------------------------------------------------
28 28 # 'ConsoleWidget' protected interface
29 29 #---------------------------------------------------------------------------
30 30
31 31 def _show_context_menu(self, pos):
32 32 """ Reimplemented to show a custom context menu for images.
33 33 """
34 34 format = self._control.cursorForPosition(pos).charFormat()
35 35 name = format.stringProperty(QtGui.QTextFormat.ImageName)
36 36 if name.isEmpty():
37 37 super(RichIPythonWidget, self)._show_context_menu(pos)
38 38 else:
39 39 menu = QtGui.QMenu()
40 40
41 41 menu.addAction('Copy Image', lambda: self._copy_image(name))
42 42 menu.addAction('Save Image As...', lambda: self._save_image(name))
43 43 menu.addSeparator()
44 44
45 45 svg = format.stringProperty(self._svg_text_format_property)
46 46 if not svg.isEmpty():
47 47 menu.addSeparator()
48 48 menu.addAction('Copy SVG', lambda: svg_to_clipboard(svg))
49 49 menu.addAction('Save SVG As...',
50 50 lambda: save_svg(svg, self._control))
51 51
52 52 menu.exec_(self._control.mapToGlobal(pos))
53 53
54 54 #---------------------------------------------------------------------------
55 55 # 'FrontendWidget' protected interface
56 56 #---------------------------------------------------------------------------
57 57
58 def _handle_execute_payload(self, payload):
59 """ Reimplemented to handle pylab plot payloads.
58 def _process_execute_ok(self, msg):
59 """ Reimplemented to handle matplotlib plot payloads.
60 60 """
61 payload = msg['content']['payload']
61 62 plot_payload = payload.get('plot', None)
62 63 if plot_payload and plot_payload['format'] == 'svg':
63 64 svg = plot_payload['data']
64 65 try:
65 66 image = svg_to_image(svg)
66 67 except ValueError:
67 68 self._append_plain_text('Received invalid plot data.')
68 69 else:
69 70 format = self._add_image(image)
70 71 format.setProperty(self._svg_text_format_property, svg)
71 72 cursor = self._get_end_cursor()
72 73 cursor.insertBlock()
73 74 cursor.insertImage(format)
74 75 cursor.insertBlock()
75 76 else:
76 super(RichIPythonWidget, self)._handle_execute_payload(payload)
77 super(RichIPythonWidget, self)._process_execute_ok(msg)
77 78
78 79 #---------------------------------------------------------------------------
79 80 # 'RichIPythonWidget' protected interface
80 81 #---------------------------------------------------------------------------
81 82
82 83 def _add_image(self, image):
83 84 """ Adds the specified QImage to the document and returns a
84 85 QTextImageFormat that references it.
85 86 """
86 87 document = self._control.document()
87 88 name = QtCore.QString.number(image.cacheKey())
88 89 document.addResource(QtGui.QTextDocument.ImageResource,
89 90 QtCore.QUrl(name), image)
90 91 format = QtGui.QTextImageFormat()
91 92 format.setName(name)
92 93 return format
93 94
94 95 def _copy_image(self, name):
95 96 """ Copies the ImageResource with 'name' to the clipboard.
96 97 """
97 98 image = self._get_image(name)
98 99 QtGui.QApplication.clipboard().setImage(image)
99 100
100 101 def _get_image(self, name):
101 102 """ Returns the QImage stored as the ImageResource with 'name'.
102 103 """
103 104 document = self._control.document()
104 105 variant = document.resource(QtGui.QTextDocument.ImageResource,
105 106 QtCore.QUrl(name))
106 107 return variant.toPyObject()
107 108
108 109 def _save_image(self, name, format='PNG'):
109 110 """ Shows a save dialog for the ImageResource with 'name'.
110 111 """
111 112 dialog = QtGui.QFileDialog(self._control, 'Save Image')
112 113 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
113 114 dialog.setDefaultSuffix(format.lower())
114 115 dialog.setNameFilter('%s file (*.%s)' % (format, format.lower()))
115 116 if dialog.exec_():
116 117 filename = dialog.selectedFiles()[0]
117 118 image = self._get_image(name)
118 119 image.save(filename, format)
@@ -1,180 +1,192 b''
1 1 """ Defines a KernelManager that provides signals and slots.
2 2 """
3 3
4 4 # System library imports.
5 5 from PyQt4 import QtCore
6 6 import zmq
7 7
8 8 # IPython imports.
9 9 from IPython.zmq.kernelmanager import KernelManager, SubSocketChannel, \
10 10 XReqSocketChannel, RepSocketChannel
11 11 from util import MetaQObjectHasTraits
12 12
13 13 # When doing multiple inheritance from QtCore.QObject and other classes
14 14 # the calling of the parent __init__'s is a subtle issue:
15 15 # * QtCore.QObject does not call super so you can't use super and put
16 16 # QObject first in the inheritance list.
17 17 # * QtCore.QObject.__init__ takes 1 argument, the parent. So if you are going
18 18 # to use super, any class that comes before QObject must pass it something
19 19 # reasonable.
20 20 # In summary, I don't think using super in these situations will work.
21 21 # Instead we will need to call the __init__ methods of both parents
22 22 # by hand. Not pretty, but it works.
23 23
24 24 class QtSubSocketChannel(SubSocketChannel, QtCore.QObject):
25 25
26 26 # Emitted when any message is received.
27 27 message_received = QtCore.pyqtSignal(object)
28 28
29 # Emitted when a message of type 'pyout' or 'stdout' is received.
30 output_received = QtCore.pyqtSignal(object)
29 # Emitted when a message of type 'stream' is received.
30 stream_received = QtCore.pyqtSignal(object)
31 31
32 # Emitted when a message of type 'pyerr' or 'stderr' is received.
33 error_received = QtCore.pyqtSignal(object)
32 # Emitted when a message of type 'pyin' is received.
33 pyin_received = QtCore.pyqtSignal(object)
34
35 # Emitted when a message of type 'pyout' is received.
36 pyout_received = QtCore.pyqtSignal(object)
37
38 # Emitted when a message of type 'pyerr' is received.
39 pyerr_received = QtCore.pyqtSignal(object)
40
41 # Emitted when a crash report message is received from the kernel's
42 # last-resort sys.excepthook.
43 crash_received = QtCore.pyqtSignal(object)
34 44
35 45 #---------------------------------------------------------------------------
36 46 # 'object' interface
37 47 #---------------------------------------------------------------------------
38 48
39 49 def __init__(self, *args, **kw):
40 50 """ Reimplemented to ensure that QtCore.QObject is initialized first.
41 51 """
42 52 QtCore.QObject.__init__(self)
43 53 SubSocketChannel.__init__(self, *args, **kw)
44 54
45 55 #---------------------------------------------------------------------------
46 56 # 'SubSocketChannel' interface
47 57 #---------------------------------------------------------------------------
48 58
49 59 def call_handlers(self, msg):
50 60 """ Reimplemented to emit signals instead of making callbacks.
51 61 """
52 62 # Emit the generic signal.
53 63 self.message_received.emit(msg)
54 64
55 65 # Emit signals for specialized message types.
56 66 msg_type = msg['msg_type']
57 if msg_type in ('pyout', 'stdout'):
58 self.output_received.emit(msg)
59 elif msg_type in ('pyerr', 'stderr'):
60 self.error_received.emit(msg)
67 signal = getattr(self, msg_type + '_received', None)
68 if signal:
69 signal.emit(msg)
70 elif msg_type in ('stdout', 'stderr'):
71 self.stream_received.emit(msg)
61 72
62 73 def flush(self):
63 74 """ Reimplemented to ensure that signals are dispatched immediately.
64 75 """
65 76 super(QtSubSocketChannel, self).flush()
66 77 QtCore.QCoreApplication.instance().processEvents()
67 78
68 79
69 80 class QtXReqSocketChannel(XReqSocketChannel, QtCore.QObject):
70 81
71 82 # Emitted when any message is received.
72 83 message_received = QtCore.pyqtSignal(object)
73 84
74 85 # Emitted when a reply has been received for the corresponding request type.
75 86 execute_reply = QtCore.pyqtSignal(object)
76 87 complete_reply = QtCore.pyqtSignal(object)
77 88 object_info_reply = QtCore.pyqtSignal(object)
78 89
79 90 #---------------------------------------------------------------------------
80 91 # 'object' interface
81 92 #---------------------------------------------------------------------------
82 93
83 94 def __init__(self, *args, **kw):
84 95 """ Reimplemented to ensure that QtCore.QObject is initialized first.
85 96 """
86 97 QtCore.QObject.__init__(self)
87 98 XReqSocketChannel.__init__(self, *args, **kw)
88 99
89 100 #---------------------------------------------------------------------------
90 101 # 'XReqSocketChannel' interface
91 102 #---------------------------------------------------------------------------
92 103
93 104 def call_handlers(self, msg):
94 105 """ Reimplemented to emit signals instead of making callbacks.
95 106 """
96 107 # Emit the generic signal.
97 108 self.message_received.emit(msg)
98 109
99 110 # Emit signals for specialized message types.
100 111 msg_type = msg['msg_type']
101 112 signal = getattr(self, msg_type, None)
102 113 if signal:
103 114 signal.emit(msg)
104 115
105 116
106 117 class QtRepSocketChannel(RepSocketChannel, QtCore.QObject):
107 118
108 119 # Emitted when any message is received.
109 120 message_received = QtCore.pyqtSignal(object)
110 121
111 122 # Emitted when an input request is received.
112 123 input_requested = QtCore.pyqtSignal(object)
113 124
114 125 #---------------------------------------------------------------------------
115 126 # 'object' interface
116 127 #---------------------------------------------------------------------------
117 128
118 129 def __init__(self, *args, **kw):
119 130 """ Reimplemented to ensure that QtCore.QObject is initialized first.
120 131 """
121 132 QtCore.QObject.__init__(self)
122 133 RepSocketChannel.__init__(self, *args, **kw)
123 134
124 135 #---------------------------------------------------------------------------
125 136 # 'RepSocketChannel' interface
126 137 #---------------------------------------------------------------------------
127 138
128 139 def call_handlers(self, msg):
129 140 """ Reimplemented to emit signals instead of making callbacks.
130 141 """
131 142 # Emit the generic signal.
132 143 self.message_received.emit(msg)
133 144
134 145 # Emit signals for specialized message types.
135 146 msg_type = msg['msg_type']
136 147 if msg_type == 'input_request':
137 148 self.input_requested.emit(msg)
138 149
150
139 151 class QtKernelManager(KernelManager, QtCore.QObject):
140 152 """ A KernelManager that provides signals and slots.
141 153 """
142 154
143 155 __metaclass__ = MetaQObjectHasTraits
144 156
145 157 # Emitted when the kernel manager has started listening.
146 158 started_channels = QtCore.pyqtSignal()
147 159
148 160 # Emitted when the kernel manager has stopped listening.
149 161 stopped_channels = QtCore.pyqtSignal()
150 162
151 163 # Use Qt-specific channel classes that emit signals.
152 164 sub_channel_class = QtSubSocketChannel
153 165 xreq_channel_class = QtXReqSocketChannel
154 166 rep_channel_class = QtRepSocketChannel
155 167
156 168 #---------------------------------------------------------------------------
157 169 # 'object' interface
158 170 #---------------------------------------------------------------------------
159 171
160 172 def __init__(self, *args, **kw):
161 173 """ Reimplemented to ensure that QtCore.QObject is initialized first.
162 174 """
163 175 QtCore.QObject.__init__(self)
164 176 KernelManager.__init__(self, *args, **kw)
165 177
166 178 #---------------------------------------------------------------------------
167 179 # 'KernelManager' interface
168 180 #---------------------------------------------------------------------------
169 181
170 182 def start_channels(self):
171 183 """ Reimplemented to emit signal.
172 184 """
173 185 super(QtKernelManager, self).start_channels()
174 186 self.started_channels.emit()
175 187
176 188 def stop_channels(self):
177 189 """ Reimplemented to emit signal.
178 190 """
179 191 super(QtKernelManager, self).stop_channels()
180 192 self.stopped_channels.emit()
General Comments 0
You need to be logged in to leave comments. Login now