##// END OF EJS Templates
* Refactored payload handling mechanism....
epatters -
Show More
@@ -1,367 +1,377 b''
1 # Standard library imports
1 # Standard library imports
2 import signal
2 import signal
3 import sys
3 import sys
4
4
5 # System library imports
5 # System library imports
6 from pygments.lexers import PythonLexer
6 from pygments.lexers import PythonLexer
7 from PyQt4 import QtCore, QtGui
7 from PyQt4 import QtCore, QtGui
8 import zmq
8 import zmq
9
9
10 # Local imports
10 # Local imports
11 from IPython.core.inputsplitter import InputSplitter
11 from IPython.core.inputsplitter import InputSplitter
12 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
12 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
13 from call_tip_widget import CallTipWidget
13 from call_tip_widget import CallTipWidget
14 from completion_lexer import CompletionLexer
14 from completion_lexer import CompletionLexer
15 from console_widget import HistoryConsoleWidget
15 from console_widget import HistoryConsoleWidget
16 from pygments_highlighter import PygmentsHighlighter
16 from pygments_highlighter import PygmentsHighlighter
17
17
18
18
19 class FrontendHighlighter(PygmentsHighlighter):
19 class FrontendHighlighter(PygmentsHighlighter):
20 """ A PygmentsHighlighter that can be turned on and off and that ignores
20 """ A PygmentsHighlighter that can be turned on and off and that ignores
21 prompts.
21 prompts.
22 """
22 """
23
23
24 def __init__(self, frontend):
24 def __init__(self, frontend):
25 super(FrontendHighlighter, self).__init__(frontend._control.document())
25 super(FrontendHighlighter, self).__init__(frontend._control.document())
26 self._current_offset = 0
26 self._current_offset = 0
27 self._frontend = frontend
27 self._frontend = frontend
28 self.highlighting_on = False
28 self.highlighting_on = False
29
29
30 def highlightBlock(self, qstring):
30 def highlightBlock(self, qstring):
31 """ Highlight a block of text. Reimplemented to highlight selectively.
31 """ Highlight a block of text. Reimplemented to highlight selectively.
32 """
32 """
33 if not self.highlighting_on:
33 if not self.highlighting_on:
34 return
34 return
35
35
36 # The input to this function is unicode string that may contain
36 # The input to this function is unicode string that may contain
37 # paragraph break characters, non-breaking spaces, etc. Here we acquire
37 # paragraph break characters, non-breaking spaces, etc. Here we acquire
38 # the string as plain text so we can compare it.
38 # the string as plain text so we can compare it.
39 current_block = self.currentBlock()
39 current_block = self.currentBlock()
40 string = self._frontend._get_block_plain_text(current_block)
40 string = self._frontend._get_block_plain_text(current_block)
41
41
42 # Decide whether to check for the regular or continuation prompt.
42 # Decide whether to check for the regular or continuation prompt.
43 if current_block.contains(self._frontend._prompt_pos):
43 if current_block.contains(self._frontend._prompt_pos):
44 prompt = self._frontend._prompt
44 prompt = self._frontend._prompt
45 else:
45 else:
46 prompt = self._frontend._continuation_prompt
46 prompt = self._frontend._continuation_prompt
47
47
48 # Don't highlight the part of the string that contains the prompt.
48 # Don't highlight the part of the string that contains the prompt.
49 if string.startswith(prompt):
49 if string.startswith(prompt):
50 self._current_offset = len(prompt)
50 self._current_offset = len(prompt)
51 qstring.remove(0, len(prompt))
51 qstring.remove(0, len(prompt))
52 else:
52 else:
53 self._current_offset = 0
53 self._current_offset = 0
54
54
55 PygmentsHighlighter.highlightBlock(self, qstring)
55 PygmentsHighlighter.highlightBlock(self, qstring)
56
56
57 def rehighlightBlock(self, block):
57 def rehighlightBlock(self, block):
58 """ Reimplemented to temporarily enable highlighting if disabled.
58 """ Reimplemented to temporarily enable highlighting if disabled.
59 """
59 """
60 old = self.highlighting_on
60 old = self.highlighting_on
61 self.highlighting_on = True
61 self.highlighting_on = True
62 super(FrontendHighlighter, self).rehighlightBlock(block)
62 super(FrontendHighlighter, self).rehighlightBlock(block)
63 self.highlighting_on = old
63 self.highlighting_on = old
64
64
65 def setFormat(self, start, count, format):
65 def setFormat(self, start, count, format):
66 """ Reimplemented to highlight selectively.
66 """ Reimplemented to highlight selectively.
67 """
67 """
68 start += self._current_offset
68 start += self._current_offset
69 PygmentsHighlighter.setFormat(self, start, count, format)
69 PygmentsHighlighter.setFormat(self, start, count, format)
70
70
71
71
72 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
72 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
73 """ A Qt frontend for a generic Python kernel.
73 """ A Qt frontend for a generic Python kernel.
74 """
74 """
75
75
76 # Emitted when an 'execute_reply' has been received from the kernel and
76 # Emitted when an 'execute_reply' has been received from the kernel and
77 # processed by the FrontendWidget.
77 # processed by the FrontendWidget.
78 executed = QtCore.pyqtSignal(object)
78 executed = QtCore.pyqtSignal(object)
79
79
80 # Protected class attributes.
80 # Protected class attributes.
81 _highlighter_class = FrontendHighlighter
81 _highlighter_class = FrontendHighlighter
82 _input_splitter_class = InputSplitter
82 _input_splitter_class = InputSplitter
83
83
84 #---------------------------------------------------------------------------
84 #---------------------------------------------------------------------------
85 # 'object' interface
85 # 'object' interface
86 #---------------------------------------------------------------------------
86 #---------------------------------------------------------------------------
87
87
88 def __init__(self, *args, **kw):
88 def __init__(self, *args, **kw):
89 super(FrontendWidget, self).__init__(*args, **kw)
89 super(FrontendWidget, self).__init__(*args, **kw)
90
90
91 # FrontendWidget protected variables.
91 # FrontendWidget protected variables.
92 self._call_tip_widget = CallTipWidget(self._control)
92 self._call_tip_widget = CallTipWidget(self._control)
93 self._completion_lexer = CompletionLexer(PythonLexer())
93 self._completion_lexer = CompletionLexer(PythonLexer())
94 self._hidden = False
94 self._hidden = False
95 self._highlighter = self._highlighter_class(self)
95 self._highlighter = self._highlighter_class(self)
96 self._input_splitter = self._input_splitter_class(input_mode='replace')
96 self._input_splitter = self._input_splitter_class(input_mode='replace')
97 self._kernel_manager = None
97 self._kernel_manager = None
98
98
99 # Configure the ConsoleWidget.
99 # Configure the ConsoleWidget.
100 self.tab_width = 4
100 self.tab_width = 4
101 self._set_continuation_prompt('... ')
101 self._set_continuation_prompt('... ')
102
102
103 # Connect signal handlers.
103 # Connect signal handlers.
104 document = self._control.document()
104 document = self._control.document()
105 document.contentsChange.connect(self._document_contents_change)
105 document.contentsChange.connect(self._document_contents_change)
106
106
107 #---------------------------------------------------------------------------
107 #---------------------------------------------------------------------------
108 # 'ConsoleWidget' abstract interface
108 # 'ConsoleWidget' abstract interface
109 #---------------------------------------------------------------------------
109 #---------------------------------------------------------------------------
110
110
111 def _is_complete(self, source, interactive):
111 def _is_complete(self, source, interactive):
112 """ Returns whether 'source' can be completely processed and a new
112 """ Returns whether 'source' can be completely processed and a new
113 prompt created. When triggered by an Enter/Return key press,
113 prompt created. When triggered by an Enter/Return key press,
114 'interactive' is True; otherwise, it is False.
114 'interactive' is True; otherwise, it is False.
115 """
115 """
116 complete = self._input_splitter.push(source.expandtabs(4))
116 complete = self._input_splitter.push(source.expandtabs(4))
117 if interactive:
117 if interactive:
118 complete = not self._input_splitter.push_accepts_more()
118 complete = not self._input_splitter.push_accepts_more()
119 return complete
119 return complete
120
120
121 def _execute(self, source, hidden):
121 def _execute(self, source, hidden):
122 """ Execute 'source'. If 'hidden', do not show any output.
122 """ Execute 'source'. If 'hidden', do not show any output.
123 """
123 """
124 self.kernel_manager.xreq_channel.execute(source)
124 self.kernel_manager.xreq_channel.execute(source)
125 self._hidden = hidden
125 self._hidden = hidden
126
126
127 def _execute_interrupt(self):
127 def _execute_interrupt(self):
128 """ Attempts to stop execution. Returns whether this method has an
128 """ Attempts to stop execution. Returns whether this method has an
129 implementation.
129 implementation.
130 """
130 """
131 self._interrupt_kernel()
131 self._interrupt_kernel()
132 return True
132 return True
133
133
134 def _prompt_started_hook(self):
134 def _prompt_started_hook(self):
135 """ Called immediately after a new prompt is displayed.
135 """ Called immediately after a new prompt is displayed.
136 """
136 """
137 if not self._reading:
137 if not self._reading:
138 self._highlighter.highlighting_on = True
138 self._highlighter.highlighting_on = True
139
139
140 def _prompt_finished_hook(self):
140 def _prompt_finished_hook(self):
141 """ Called immediately after a prompt is finished, i.e. when some input
141 """ Called immediately after a prompt is finished, i.e. when some input
142 will be processed and a new prompt displayed.
142 will be processed and a new prompt displayed.
143 """
143 """
144 if not self._reading:
144 if not self._reading:
145 self._highlighter.highlighting_on = False
145 self._highlighter.highlighting_on = False
146
146
147 def _tab_pressed(self):
147 def _tab_pressed(self):
148 """ Called when the tab key is pressed. Returns whether to continue
148 """ Called when the tab key is pressed. Returns whether to continue
149 processing the event.
149 processing the event.
150 """
150 """
151 self._keep_cursor_in_buffer()
151 self._keep_cursor_in_buffer()
152 cursor = self._get_cursor()
152 cursor = self._get_cursor()
153 return not self._complete()
153 return not self._complete()
154
154
155 #---------------------------------------------------------------------------
155 #---------------------------------------------------------------------------
156 # 'ConsoleWidget' protected interface
156 # 'ConsoleWidget' protected interface
157 #---------------------------------------------------------------------------
157 #---------------------------------------------------------------------------
158
158
159 def _show_continuation_prompt(self):
159 def _show_continuation_prompt(self):
160 """ Reimplemented for auto-indentation.
160 """ Reimplemented for auto-indentation.
161 """
161 """
162 super(FrontendWidget, self)._show_continuation_prompt()
162 super(FrontendWidget, self)._show_continuation_prompt()
163 spaces = self._input_splitter.indent_spaces
163 spaces = self._input_splitter.indent_spaces
164 self._append_plain_text('\t' * (spaces / self.tab_width))
164 self._append_plain_text('\t' * (spaces / self.tab_width))
165 self._append_plain_text(' ' * (spaces % self.tab_width))
165 self._append_plain_text(' ' * (spaces % self.tab_width))
166
166
167 #---------------------------------------------------------------------------
167 #---------------------------------------------------------------------------
168 # 'BaseFrontendMixin' abstract interface
168 # 'BaseFrontendMixin' abstract interface
169 #---------------------------------------------------------------------------
169 #---------------------------------------------------------------------------
170
170
171 def _handle_complete_reply(self, rep):
171 def _handle_complete_reply(self, rep):
172 """ Handle replies for tab completion.
172 """ Handle replies for tab completion.
173 """
173 """
174 cursor = self._get_cursor()
174 cursor = self._get_cursor()
175 if rep['parent_header']['msg_id'] == self._complete_id and \
175 if rep['parent_header']['msg_id'] == self._complete_id and \
176 cursor.position() == self._complete_pos:
176 cursor.position() == self._complete_pos:
177 text = '.'.join(self._get_context())
177 text = '.'.join(self._get_context())
178 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
178 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
179 self._complete_with_items(cursor, rep['content']['matches'])
179 self._complete_with_items(cursor, rep['content']['matches'])
180
180
181 def _handle_execute_reply(self, msg):
181 def _handle_execute_reply(self, msg):
182 """ Handles replies for code execution.
182 """ Handles replies for code execution.
183 """
183 """
184 if not self._hidden:
184 if not self._hidden:
185 # Make sure that all output from the SUB channel has been processed
185 # Make sure that all output from the SUB channel has been processed
186 # before writing a new prompt.
186 # before writing a new prompt.
187 self.kernel_manager.sub_channel.flush()
187 self.kernel_manager.sub_channel.flush()
188
188
189 content = msg['content']
189 content = msg['content']
190 status = content['status']
190 status = content['status']
191 if status == 'ok':
191 if status == 'ok':
192 self._process_execute_ok(msg)
192 self._process_execute_ok(msg)
193 elif status == 'error':
193 elif status == 'error':
194 self._process_execute_error(msg)
194 self._process_execute_error(msg)
195 elif status == 'abort':
195 elif status == 'abort':
196 self._process_execute_abort(msg)
196 self._process_execute_abort(msg)
197
197
198 self._show_interpreter_prompt_for_reply(msg)
198 self._show_interpreter_prompt_for_reply(msg)
199 self.executed.emit(msg)
199 self.executed.emit(msg)
200
200
201 def _handle_input_request(self, msg):
201 def _handle_input_request(self, msg):
202 """ Handle requests for raw_input.
202 """ Handle requests for raw_input.
203 """
203 """
204 if self._hidden:
204 if self._hidden:
205 raise RuntimeError('Request for raw input during hidden execution.')
205 raise RuntimeError('Request for raw input during hidden execution.')
206
206
207 # Make sure that all output from the SUB channel has been processed
207 # Make sure that all output from the SUB channel has been processed
208 # before entering readline mode.
208 # before entering readline mode.
209 self.kernel_manager.sub_channel.flush()
209 self.kernel_manager.sub_channel.flush()
210
210
211 def callback(line):
211 def callback(line):
212 self.kernel_manager.rep_channel.input(line)
212 self.kernel_manager.rep_channel.input(line)
213 self._readline(msg['content']['prompt'], callback=callback)
213 self._readline(msg['content']['prompt'], callback=callback)
214
214
215 def _handle_object_info_reply(self, rep):
215 def _handle_object_info_reply(self, rep):
216 """ Handle replies for call tips.
216 """ Handle replies for call tips.
217 """
217 """
218 cursor = self._get_cursor()
218 cursor = self._get_cursor()
219 if rep['parent_header']['msg_id'] == self._call_tip_id and \
219 if rep['parent_header']['msg_id'] == self._call_tip_id and \
220 cursor.position() == self._call_tip_pos:
220 cursor.position() == self._call_tip_pos:
221 doc = rep['content']['docstring']
221 doc = rep['content']['docstring']
222 if doc:
222 if doc:
223 self._call_tip_widget.show_docstring(doc)
223 self._call_tip_widget.show_docstring(doc)
224
224
225 def _handle_pyout(self, msg):
225 def _handle_pyout(self, msg):
226 """ Handle display hook output.
226 """ Handle display hook output.
227 """
227 """
228 if not self._hidden and self._is_from_this_session(msg):
228 if not self._hidden and self._is_from_this_session(msg):
229 self._append_plain_text(msg['content']['data'] + '\n')
229 self._append_plain_text(msg['content']['data'] + '\n')
230
230
231 def _handle_stream(self, msg):
231 def _handle_stream(self, msg):
232 """ Handle stdout, stderr, and stdin.
232 """ Handle stdout, stderr, and stdin.
233 """
233 """
234 if not self._hidden and self._is_from_this_session(msg):
234 if not self._hidden and self._is_from_this_session(msg):
235 self._append_plain_text(msg['content']['data'])
235 self._append_plain_text(msg['content']['data'])
236 self._control.moveCursor(QtGui.QTextCursor.End)
236 self._control.moveCursor(QtGui.QTextCursor.End)
237
237
238 def _started_channels(self):
238 def _started_channels(self):
239 """ Called when the KernelManager channels have started listening or
239 """ Called when the KernelManager channels have started listening or
240 when the frontend is assigned an already listening KernelManager.
240 when the frontend is assigned an already listening KernelManager.
241 """
241 """
242 self._reset()
242 self._reset()
243 self._append_plain_text(self._get_banner())
243 self._append_plain_text(self._get_banner())
244 self._show_interpreter_prompt()
244 self._show_interpreter_prompt()
245
245
246 def _stopped_channels(self):
246 def _stopped_channels(self):
247 """ Called when the KernelManager channels have stopped listening or
247 """ Called when the KernelManager channels have stopped listening or
248 when a listening KernelManager is removed from the frontend.
248 when a listening KernelManager is removed from the frontend.
249 """
249 """
250 # FIXME: Print a message here?
250 # FIXME: Print a message here?
251 pass
251 pass
252
252
253 #---------------------------------------------------------------------------
253 #---------------------------------------------------------------------------
254 # 'FrontendWidget' interface
254 # 'FrontendWidget' interface
255 #---------------------------------------------------------------------------
255 #---------------------------------------------------------------------------
256
256
257 def execute_file(self, path, hidden=False):
257 def execute_file(self, path, hidden=False):
258 """ Attempts to execute file with 'path'. If 'hidden', no output is
258 """ Attempts to execute file with 'path'. If 'hidden', no output is
259 shown.
259 shown.
260 """
260 """
261 self.execute('execfile("%s")' % path, hidden=hidden)
261 self.execute('execfile("%s")' % path, hidden=hidden)
262
262
263 #---------------------------------------------------------------------------
263 #---------------------------------------------------------------------------
264 # 'FrontendWidget' protected interface
264 # 'FrontendWidget' protected interface
265 #---------------------------------------------------------------------------
265 #---------------------------------------------------------------------------
266
266
267 def _call_tip(self):
267 def _call_tip(self):
268 """ Shows a call tip, if appropriate, at the current cursor location.
268 """ Shows a call tip, if appropriate, at the current cursor location.
269 """
269 """
270 # Decide if it makes sense to show a call tip
270 # Decide if it makes sense to show a call tip
271 cursor = self._get_cursor()
271 cursor = self._get_cursor()
272 cursor.movePosition(QtGui.QTextCursor.Left)
272 cursor.movePosition(QtGui.QTextCursor.Left)
273 document = self._control.document()
273 document = self._control.document()
274 if document.characterAt(cursor.position()).toAscii() != '(':
274 if document.characterAt(cursor.position()).toAscii() != '(':
275 return False
275 return False
276 context = self._get_context(cursor)
276 context = self._get_context(cursor)
277 if not context:
277 if not context:
278 return False
278 return False
279
279
280 # Send the metadata request to the kernel
280 # Send the metadata request to the kernel
281 name = '.'.join(context)
281 name = '.'.join(context)
282 self._call_tip_id = self.kernel_manager.xreq_channel.object_info(name)
282 self._call_tip_id = self.kernel_manager.xreq_channel.object_info(name)
283 self._call_tip_pos = self._get_cursor().position()
283 self._call_tip_pos = self._get_cursor().position()
284 return True
284 return True
285
285
286 def _complete(self):
286 def _complete(self):
287 """ Performs completion at the current cursor location.
287 """ Performs completion at the current cursor location.
288 """
288 """
289 # Decide if it makes sense to do completion
289 # Decide if it makes sense to do completion
290 context = self._get_context()
290 context = self._get_context()
291 if not context:
291 if not context:
292 return False
292 return False
293
293
294 # Send the completion request to the kernel
294 # Send the completion request to the kernel
295 text = '.'.join(context)
295 text = '.'.join(context)
296 self._complete_id = self.kernel_manager.xreq_channel.complete(
296 self._complete_id = self.kernel_manager.xreq_channel.complete(
297 text, self._get_input_buffer_cursor_line(), self.input_buffer)
297 text, self._get_input_buffer_cursor_line(), self.input_buffer)
298 self._complete_pos = self._get_cursor().position()
298 self._complete_pos = self._get_cursor().position()
299 return True
299 return True
300
300
301 def _get_banner(self):
301 def _get_banner(self):
302 """ Gets a banner to display at the beginning of a session.
302 """ Gets a banner to display at the beginning of a session.
303 """
303 """
304 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
304 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
305 '"license" for more information.'
305 '"license" for more information.'
306 return banner % (sys.version, sys.platform)
306 return banner % (sys.version, sys.platform)
307
307
308 def _get_context(self, cursor=None):
308 def _get_context(self, cursor=None):
309 """ Gets the context at the current cursor location.
309 """ Gets the context at the current cursor location.
310 """
310 """
311 if cursor is None:
311 if cursor is None:
312 cursor = self._get_cursor()
312 cursor = self._get_cursor()
313 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
313 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
314 QtGui.QTextCursor.KeepAnchor)
314 QtGui.QTextCursor.KeepAnchor)
315 text = str(cursor.selection().toPlainText())
315 text = str(cursor.selection().toPlainText())
316 return self._completion_lexer.get_context(text)
316 return self._completion_lexer.get_context(text)
317
317
318 def _interrupt_kernel(self):
318 def _interrupt_kernel(self):
319 """ Attempts to the interrupt the kernel.
319 """ Attempts to the interrupt the kernel.
320 """
320 """
321 if self.kernel_manager.has_kernel:
321 if self.kernel_manager.has_kernel:
322 self.kernel_manager.signal_kernel(signal.SIGINT)
322 self.kernel_manager.signal_kernel(signal.SIGINT)
323 else:
323 else:
324 self._append_plain_text('Kernel process is either remote or '
324 self._append_plain_text('Kernel process is either remote or '
325 'unspecified. Cannot interrupt.\n')
325 'unspecified. Cannot interrupt.\n')
326
326
327 def _process_execute_abort(self, msg):
327 def _process_execute_abort(self, msg):
328 """ Process a reply for an aborted execution request.
328 """ Process a reply for an aborted execution request.
329 """
329 """
330 self._append_plain_text("ERROR: execution aborted\n")
330 self._append_plain_text("ERROR: execution aborted\n")
331
331
332 def _process_execute_error(self, msg):
332 def _process_execute_error(self, msg):
333 """ Process a reply for an execution request that resulted in an error.
333 """ Process a reply for an execution request that resulted in an error.
334 """
334 """
335 content = msg['content']
335 content = msg['content']
336 traceback = ''.join(content['traceback'])
336 traceback = ''.join(content['traceback'])
337 self._append_plain_text(traceback)
337 self._append_plain_text(traceback)
338
338
339 def _process_execute_ok(self, msg):
339 def _process_execute_ok(self, msg):
340 """ Process a reply for a successful execution equest.
340 """ Process a reply for a successful execution equest.
341 """
341 """
342 payload = msg['content']['payload']
343 for item in payload:
344 if not self._process_execute_payload(item):
345 warning = 'Received unknown payload of type %s\n'
346 self._append_plain_text(warning % repr(item['source']))
347
348 def _process_execute_payload(self, item):
349 """ Process a single payload item from the list of payload items in an
350 execution reply. Returns whether the payload was handled.
351 """
342 # The basic FrontendWidget doesn't handle payloads, as they are a
352 # The basic FrontendWidget doesn't handle payloads, as they are a
343 # mechanism for going beyond the standard Python interpreter model.
353 # mechanism for going beyond the standard Python interpreter model.
344 pass
354 return False
345
355
346 def _show_interpreter_prompt(self):
356 def _show_interpreter_prompt(self):
347 """ Shows a prompt for the interpreter.
357 """ Shows a prompt for the interpreter.
348 """
358 """
349 self._show_prompt('>>> ')
359 self._show_prompt('>>> ')
350
360
351 def _show_interpreter_prompt_for_reply(self, msg):
361 def _show_interpreter_prompt_for_reply(self, msg):
352 """ Shows a prompt for the interpreter given an 'execute_reply' message.
362 """ Shows a prompt for the interpreter given an 'execute_reply' message.
353 """
363 """
354 self._show_interpreter_prompt()
364 self._show_interpreter_prompt()
355
365
356 #------ Signal handlers ----------------------------------------------------
366 #------ Signal handlers ----------------------------------------------------
357
367
358 def _document_contents_change(self, position, removed, added):
368 def _document_contents_change(self, position, removed, added):
359 """ Called whenever the document's content changes. Display a call tip
369 """ Called whenever the document's content changes. Display a call tip
360 if appropriate.
370 if appropriate.
361 """
371 """
362 # Calculate where the cursor should be *after* the change:
372 # Calculate where the cursor should be *after* the change:
363 position += added
373 position += added
364
374
365 document = self._control.document()
375 document = self._control.document()
366 if position == self._get_cursor().position():
376 if position == self._get_cursor().position():
367 self._call_tip()
377 self._call_tip()
@@ -1,274 +1,293 b''
1 # Standard library imports
1 # Standard library imports
2 from subprocess import Popen
2 from subprocess import Popen
3
3
4 # System library imports
4 # System library imports
5 from PyQt4 import QtCore, QtGui
5 from PyQt4 import QtCore, QtGui
6
6
7 # Local imports
7 # Local imports
8 from IPython.core.inputsplitter import IPythonInputSplitter
8 from IPython.core.inputsplitter import IPythonInputSplitter
9 from IPython.core.usage import default_banner
9 from IPython.core.usage import default_banner
10 from frontend_widget import FrontendWidget
10 from frontend_widget import FrontendWidget
11
11
12
12
13 class IPythonPromptBlock(object):
13 class IPythonPromptBlock(object):
14 """ An internal storage object for IPythonWidget.
14 """ An internal storage object for IPythonWidget.
15 """
15 """
16 def __init__(self, block, length, number):
16 def __init__(self, block, length, number):
17 self.block = block
17 self.block = block
18 self.length = length
18 self.length = length
19 self.number = number
19 self.number = number
20
20
21
21
22 class IPythonWidget(FrontendWidget):
22 class IPythonWidget(FrontendWidget):
23 """ A FrontendWidget for an IPython kernel.
23 """ A FrontendWidget for an IPython kernel.
24 """
24 """
25
25
26 # Signal emitted when an editor is needed for a file and the editor has been
26 # Signal emitted when an editor is needed for a file and the editor has been
27 # specified as 'custom'.
27 # specified as 'custom'. See 'set_editor' for more information.
28 custom_edit_requested = QtCore.pyqtSignal(object)
28 custom_edit_requested = QtCore.pyqtSignal(object, int)
29
29
30 # The default stylesheet: black text on a white background.
30 # The default stylesheet: black text on a white background.
31 default_stylesheet = """
31 default_stylesheet = """
32 .error { color: red; }
32 .error { color: red; }
33 .in-prompt { color: navy; }
33 .in-prompt { color: navy; }
34 .in-prompt-number { font-weight: bold; }
34 .in-prompt-number { font-weight: bold; }
35 .out-prompt { color: darkred; }
35 .out-prompt { color: darkred; }
36 .out-prompt-number { font-weight: bold; }
36 .out-prompt-number { font-weight: bold; }
37 """
37 """
38
38
39 # A dark stylesheet: white text on a black background.
39 # A dark stylesheet: white text on a black background.
40 dark_stylesheet = """
40 dark_stylesheet = """
41 QPlainTextEdit, QTextEdit { background-color: black; color: white }
41 QPlainTextEdit, QTextEdit { background-color: black; color: white }
42 QFrame { border: 1px solid grey; }
42 QFrame { border: 1px solid grey; }
43 .error { color: red; }
43 .error { color: red; }
44 .in-prompt { color: lime; }
44 .in-prompt { color: lime; }
45 .in-prompt-number { color: lime; font-weight: bold; }
45 .in-prompt-number { color: lime; font-weight: bold; }
46 .out-prompt { color: red; }
46 .out-prompt { color: red; }
47 .out-prompt-number { color: red; font-weight: bold; }
47 .out-prompt-number { color: red; font-weight: bold; }
48 """
48 """
49
49
50 # Default prompts.
50 # Default prompts.
51 in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
51 in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
52 out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
52 out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
53
53
54 # FrontendWidget protected class attributes.
54 # FrontendWidget protected class variables.
55 #_input_splitter_class = IPythonInputSplitter
55 #_input_splitter_class = IPythonInputSplitter
56
56
57 # IPythonWidget protected class variables.
58 _payload_source_edit = 'IPython.zmq.zmqshell.ZMQInteractiveShell.edit_magic'
59 _payload_source_page = 'IPython.zmq.page.page'
60
57 #---------------------------------------------------------------------------
61 #---------------------------------------------------------------------------
58 # 'object' interface
62 # 'object' interface
59 #---------------------------------------------------------------------------
63 #---------------------------------------------------------------------------
60
64
61 def __init__(self, *args, **kw):
65 def __init__(self, *args, **kw):
62 super(IPythonWidget, self).__init__(*args, **kw)
66 super(IPythonWidget, self).__init__(*args, **kw)
63
67
64 # IPythonWidget protected variables.
68 # IPythonWidget protected variables.
65 self._previous_prompt_obj = None
69 self._previous_prompt_obj = None
66
70
67 # Set a default editor and stylesheet.
71 # Set a default editor and stylesheet.
68 self.set_editor('default')
72 self.set_editor('default')
69 self.reset_styling()
73 self.reset_styling()
70
74
71 #---------------------------------------------------------------------------
75 #---------------------------------------------------------------------------
72 # 'BaseFrontendMixin' abstract interface
76 # 'BaseFrontendMixin' abstract interface
73 #---------------------------------------------------------------------------
77 #---------------------------------------------------------------------------
74
78
75 def _handle_pyout(self, msg):
79 def _handle_pyout(self, msg):
76 """ Reimplemented for IPython-style "display hook".
80 """ Reimplemented for IPython-style "display hook".
77 """
81 """
78 if not self._hidden and self._is_from_this_session(msg):
82 if not self._hidden and self._is_from_this_session(msg):
79 content = msg['content']
83 content = msg['content']
80 prompt_number = content['prompt_number']
84 prompt_number = content['prompt_number']
81 self._append_plain_text(content['output_sep'])
85 self._append_plain_text(content['output_sep'])
82 self._append_html(self._make_out_prompt(prompt_number))
86 self._append_html(self._make_out_prompt(prompt_number))
83 self._append_plain_text(content['data'] + '\n' +
87 self._append_plain_text(content['data'] + '\n' +
84 content['output_sep2'])
88 content['output_sep2'])
85
89
86 #---------------------------------------------------------------------------
90 #---------------------------------------------------------------------------
87 # 'FrontendWidget' interface
91 # 'FrontendWidget' interface
88 #---------------------------------------------------------------------------
92 #---------------------------------------------------------------------------
89
93
90 def execute_file(self, path, hidden=False):
94 def execute_file(self, path, hidden=False):
91 """ Reimplemented to use the 'run' magic.
95 """ Reimplemented to use the 'run' magic.
92 """
96 """
93 self.execute('run %s' % path, hidden=hidden)
97 self.execute('run %s' % path, hidden=hidden)
94
98
95 #---------------------------------------------------------------------------
99 #---------------------------------------------------------------------------
96 # 'FrontendWidget' protected interface
100 # 'FrontendWidget' protected interface
97 #---------------------------------------------------------------------------
101 #---------------------------------------------------------------------------
98
102
99 def _get_banner(self):
103 def _get_banner(self):
100 """ Reimplemented to return IPython's default banner.
104 """ Reimplemented to return IPython's default banner.
101 """
105 """
102 return default_banner + '\n'
106 return default_banner + '\n'
103
107
104 def _process_execute_error(self, msg):
108 def _process_execute_error(self, msg):
105 """ Reimplemented for IPython-style traceback formatting.
109 """ Reimplemented for IPython-style traceback formatting.
106 """
110 """
107 content = msg['content']
111 content = msg['content']
108 traceback_lines = content['traceback'][:]
112 traceback_lines = content['traceback'][:]
109 traceback = ''.join(traceback_lines)
113 traceback = ''.join(traceback_lines)
110 traceback = traceback.replace(' ', '&nbsp;')
114 traceback = traceback.replace(' ', '&nbsp;')
111 traceback = traceback.replace('\n', '<br/>')
115 traceback = traceback.replace('\n', '<br/>')
112
116
113 ename = content['ename']
117 ename = content['ename']
114 ename_styled = '<span class="error">%s</span>' % ename
118 ename_styled = '<span class="error">%s</span>' % ename
115 traceback = traceback.replace(ename, ename_styled)
119 traceback = traceback.replace(ename, ename_styled)
116
120
117 self._append_html(traceback)
121 self._append_html(traceback)
118
122
123 def _process_execute_payload(self, item):
124 """ Reimplemented to handle %edit and paging payloads.
125 """
126 if item['source'] == self._payload_source_edit:
127 self.edit(item['filename'], item['line_number'])
128 return True
129 elif item['source'] == self._payload_source_page:
130 self._page(item['data'])
131 return True
132 else:
133 return False
134
119 def _show_interpreter_prompt(self, number=None, input_sep='\n'):
135 def _show_interpreter_prompt(self, number=None, input_sep='\n'):
120 """ Reimplemented for IPython-style prompts.
136 """ Reimplemented for IPython-style prompts.
121 """
137 """
122 # TODO: If a number was not specified, make a prompt number request.
138 # TODO: If a number was not specified, make a prompt number request.
123 if number is None:
139 if number is None:
124 number = 0
140 number = 0
125
141
126 # Show a new prompt and save information about it so that it can be
142 # Show a new prompt and save information about it so that it can be
127 # updated later if the prompt number turns out to be wrong.
143 # updated later if the prompt number turns out to be wrong.
128 self._append_plain_text(input_sep)
144 self._append_plain_text(input_sep)
129 self._show_prompt(self._make_in_prompt(number), html=True)
145 self._show_prompt(self._make_in_prompt(number), html=True)
130 block = self._control.document().lastBlock()
146 block = self._control.document().lastBlock()
131 length = len(self._prompt)
147 length = len(self._prompt)
132 self._previous_prompt_obj = IPythonPromptBlock(block, length, number)
148 self._previous_prompt_obj = IPythonPromptBlock(block, length, number)
133
149
134 # Update continuation prompt to reflect (possibly) new prompt length.
150 # Update continuation prompt to reflect (possibly) new prompt length.
135 self._set_continuation_prompt(
151 self._set_continuation_prompt(
136 self._make_continuation_prompt(self._prompt), html=True)
152 self._make_continuation_prompt(self._prompt), html=True)
137
153
138 def _show_interpreter_prompt_for_reply(self, msg):
154 def _show_interpreter_prompt_for_reply(self, msg):
139 """ Reimplemented for IPython-style prompts.
155 """ Reimplemented for IPython-style prompts.
140 """
156 """
141 # Update the old prompt number if necessary.
157 # Update the old prompt number if necessary.
142 content = msg['content']
158 content = msg['content']
143 previous_prompt_number = content['prompt_number']
159 previous_prompt_number = content['prompt_number']
144 if self._previous_prompt_obj and \
160 if self._previous_prompt_obj and \
145 self._previous_prompt_obj.number != previous_prompt_number:
161 self._previous_prompt_obj.number != previous_prompt_number:
146 block = self._previous_prompt_obj.block
162 block = self._previous_prompt_obj.block
147 if block.isValid():
163 if block.isValid():
148
164
149 # Remove the old prompt and insert a new prompt.
165 # Remove the old prompt and insert a new prompt.
150 cursor = QtGui.QTextCursor(block)
166 cursor = QtGui.QTextCursor(block)
151 cursor.movePosition(QtGui.QTextCursor.Right,
167 cursor.movePosition(QtGui.QTextCursor.Right,
152 QtGui.QTextCursor.KeepAnchor,
168 QtGui.QTextCursor.KeepAnchor,
153 self._previous_prompt_obj.length)
169 self._previous_prompt_obj.length)
154 prompt = self._make_in_prompt(previous_prompt_number)
170 prompt = self._make_in_prompt(previous_prompt_number)
155 self._prompt = self._insert_html_fetching_plain_text(
171 self._prompt = self._insert_html_fetching_plain_text(
156 cursor, prompt)
172 cursor, prompt)
157
173
158 # When the HTML is inserted, Qt blows away the syntax
174 # When the HTML is inserted, Qt blows away the syntax
159 # highlighting for the line, so we need to rehighlight it.
175 # highlighting for the line, so we need to rehighlight it.
160 self._highlighter.rehighlightBlock(cursor.block())
176 self._highlighter.rehighlightBlock(cursor.block())
161
177
162 self._previous_prompt_obj = None
178 self._previous_prompt_obj = None
163
179
164 # Show a new prompt with the kernel's estimated prompt number.
180 # Show a new prompt with the kernel's estimated prompt number.
165 next_prompt = content['next_prompt']
181 next_prompt = content['next_prompt']
166 self._show_interpreter_prompt(next_prompt['prompt_number'],
182 self._show_interpreter_prompt(next_prompt['prompt_number'],
167 next_prompt['input_sep'])
183 next_prompt['input_sep'])
168
184
169 #---------------------------------------------------------------------------
185 #---------------------------------------------------------------------------
170 # 'IPythonWidget' interface
186 # 'IPythonWidget' interface
171 #---------------------------------------------------------------------------
187 #---------------------------------------------------------------------------
172
188
173 def edit(self, filename):
189 def edit(self, filename, line=None):
174 """ Opens a Python script for editing.
190 """ Opens a Python script for editing.
175
191
176 Parameters:
192 Parameters:
177 -----------
193 -----------
178 filename : str
194 filename : str
179 A path to a local system file.
195 A path to a local system file.
196
197 line : int, optional
198 A line of interest in the file.
180
199
181 Raises:
200 Raises:
182 -------
201 -------
183 OSError
202 OSError
184 If the editor command cannot be executed.
203 If the editor command cannot be executed.
185 """
204 """
186 if self._editor == 'default':
205 if self._editor == 'default':
187 url = QtCore.QUrl.fromLocalFile(filename)
206 url = QtCore.QUrl.fromLocalFile(filename)
188 if not QtGui.QDesktopServices.openUrl(url):
207 if not QtGui.QDesktopServices.openUrl(url):
189 message = 'Failed to open %s with the default application'
208 message = 'Failed to open %s with the default application'
190 raise OSError(message % repr(filename))
209 raise OSError(message % repr(filename))
191 elif self._editor is None:
210 elif self._editor is None:
192 self.custom_edit_requested.emit(filename)
211 self.custom_edit_requested.emit(filename, line)
193 else:
212 else:
194 Popen(self._editor + [filename])
213 Popen(self._editor + [filename])
195
214
196 def reset_styling(self):
215 def reset_styling(self):
197 """ Restores the default IPythonWidget styling.
216 """ Restores the default IPythonWidget styling.
198 """
217 """
199 self.set_styling(self.default_stylesheet, syntax_style='default')
218 self.set_styling(self.default_stylesheet, syntax_style='default')
200 #self.set_styling(self.dark_stylesheet, syntax_style='monokai')
219 #self.set_styling(self.dark_stylesheet, syntax_style='monokai')
201
220
202 def set_editor(self, editor):
221 def set_editor(self, editor):
203 """ Sets the editor to use with the %edit magic.
222 """ Sets the editor to use with the %edit magic.
204
223
205 Parameters:
224 Parameters:
206 -----------
225 -----------
207 editor : str or sequence of str
226 editor : str or sequence of str
208 A command suitable for use with Popen. This command will be executed
227 A command suitable for use with Popen. This command will be executed
209 with a single argument--a filename--when editing is requested.
228 with a single argument--a filename--when editing is requested.
210
229
211 This parameter also takes two special values:
230 This parameter also takes two special values:
212 'default' : Files will be edited with the system default
231 'default' : Files will be edited with the system default
213 application for Python files.
232 application for Python files.
214 'custom' : Emit a 'custom_edit_requested(str)' signal instead
233 'custom' : Emit a 'custom_edit_requested(str, int)' signal
215 of opening an editor.
234 instead of opening an editor.
216 """
235 """
217 if editor == 'default':
236 if editor == 'default':
218 self._editor = 'default'
237 self._editor = 'default'
219 elif editor == 'custom':
238 elif editor == 'custom':
220 self._editor = None
239 self._editor = None
221 elif isinstance(editor, basestring):
240 elif isinstance(editor, basestring):
222 self._editor = [ editor ]
241 self._editor = [ editor ]
223 else:
242 else:
224 self._editor = list(editor)
243 self._editor = list(editor)
225
244
226 def set_styling(self, stylesheet, syntax_style=None):
245 def set_styling(self, stylesheet, syntax_style=None):
227 """ Sets the IPythonWidget styling.
246 """ Sets the IPythonWidget styling.
228
247
229 Parameters:
248 Parameters:
230 -----------
249 -----------
231 stylesheet : str
250 stylesheet : str
232 A CSS stylesheet. The stylesheet can contain classes for:
251 A CSS stylesheet. The stylesheet can contain classes for:
233 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
252 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
234 2. Pygments: .c, .k, .o, etc (see PygmentsHighlighter)
253 2. Pygments: .c, .k, .o, etc (see PygmentsHighlighter)
235 3. IPython: .error, .in-prompt, .out-prompt, etc.
254 3. IPython: .error, .in-prompt, .out-prompt, etc.
236
255
237 syntax_style : str or None [default None]
256 syntax_style : str or None [default None]
238 If specified, use the Pygments style with given name. Otherwise,
257 If specified, use the Pygments style with given name. Otherwise,
239 the stylesheet is queried for Pygments style information.
258 the stylesheet is queried for Pygments style information.
240 """
259 """
241 self.setStyleSheet(stylesheet)
260 self.setStyleSheet(stylesheet)
242 self._control.document().setDefaultStyleSheet(stylesheet)
261 self._control.document().setDefaultStyleSheet(stylesheet)
243 if self._page_control:
262 if self._page_control:
244 self._page_control.document().setDefaultStyleSheet(stylesheet)
263 self._page_control.document().setDefaultStyleSheet(stylesheet)
245
264
246 if syntax_style is None:
265 if syntax_style is None:
247 self._highlighter.set_style_sheet(stylesheet)
266 self._highlighter.set_style_sheet(stylesheet)
248 else:
267 else:
249 self._highlighter.set_style(syntax_style)
268 self._highlighter.set_style(syntax_style)
250
269
251 #---------------------------------------------------------------------------
270 #---------------------------------------------------------------------------
252 # 'IPythonWidget' protected interface
271 # 'IPythonWidget' protected interface
253 #---------------------------------------------------------------------------
272 #---------------------------------------------------------------------------
254
273
255 def _make_in_prompt(self, number):
274 def _make_in_prompt(self, number):
256 """ Given a prompt number, returns an HTML In prompt.
275 """ Given a prompt number, returns an HTML In prompt.
257 """
276 """
258 body = self.in_prompt % number
277 body = self.in_prompt % number
259 return '<span class="in-prompt">%s</span>' % body
278 return '<span class="in-prompt">%s</span>' % body
260
279
261 def _make_continuation_prompt(self, prompt):
280 def _make_continuation_prompt(self, prompt):
262 """ Given a plain text version of an In prompt, returns an HTML
281 """ Given a plain text version of an In prompt, returns an HTML
263 continuation prompt.
282 continuation prompt.
264 """
283 """
265 end_chars = '...: '
284 end_chars = '...: '
266 space_count = len(prompt.lstrip('\n')) - len(end_chars)
285 space_count = len(prompt.lstrip('\n')) - len(end_chars)
267 body = '&nbsp;' * space_count + end_chars
286 body = '&nbsp;' * space_count + end_chars
268 return '<span class="in-prompt">%s</span>' % body
287 return '<span class="in-prompt">%s</span>' % body
269
288
270 def _make_out_prompt(self, number):
289 def _make_out_prompt(self, number):
271 """ Given a prompt number, returns an HTML Out prompt.
290 """ Given a prompt number, returns an HTML Out prompt.
272 """
291 """
273 body = self.out_prompt % number
292 body = self.out_prompt % number
274 return '<span class="out-prompt">%s</span>' % body
293 return '<span class="out-prompt">%s</span>' % body
@@ -1,151 +1,126 b''
1 import os
1 import os
2
2
3 # System library imports
3 # System library imports
4 from PyQt4 import QtCore, QtGui
4 from PyQt4 import QtCore, QtGui
5
5
6 # Local imports
6 # Local imports
7 from IPython.frontend.qt.svg import save_svg, svg_to_clipboard, svg_to_image
7 from IPython.frontend.qt.svg import save_svg, svg_to_clipboard, svg_to_image
8 from ipython_widget import IPythonWidget
8 from ipython_widget import IPythonWidget
9
9
10
10
11 class RichIPythonWidget(IPythonWidget):
11 class RichIPythonWidget(IPythonWidget):
12 """ An IPythonWidget that supports rich text, including lists, images, and
12 """ An IPythonWidget that supports rich text, including lists, images, and
13 tables. Note that raw performance will be reduced compared to the plain
13 tables. Note that raw performance will be reduced compared to the plain
14 text version.
14 text version.
15 """
15 """
16
16
17 # Protected class variables.
17 # RichIPythonWidget protected class variables.
18 _payload_source_plot = 'IPython.zmq.pylab.backend_payload.add_plot_payload'
18 _svg_text_format_property = 1
19 _svg_text_format_property = 1
19
20
20 #---------------------------------------------------------------------------
21 #---------------------------------------------------------------------------
21 # 'object' interface
22 # 'object' interface
22 #---------------------------------------------------------------------------
23 #---------------------------------------------------------------------------
23
24
24 def __init__(self, *args, **kw):
25 def __init__(self, *args, **kw):
25 """ Create a RichIPythonWidget.
26 """ Create a RichIPythonWidget.
26 """
27 """
27 kw['kind'] = 'rich'
28 kw['kind'] = 'rich'
28 super(RichIPythonWidget, self).__init__(*args, **kw)
29 super(RichIPythonWidget, self).__init__(*args, **kw)
29
30
30 #---------------------------------------------------------------------------
31 #---------------------------------------------------------------------------
31 # 'ConsoleWidget' protected interface
32 # 'ConsoleWidget' protected interface
32 #---------------------------------------------------------------------------
33 #---------------------------------------------------------------------------
33
34
34 def _show_context_menu(self, pos):
35 def _show_context_menu(self, pos):
35 """ Reimplemented to show a custom context menu for images.
36 """ Reimplemented to show a custom context menu for images.
36 """
37 """
37 format = self._control.cursorForPosition(pos).charFormat()
38 format = self._control.cursorForPosition(pos).charFormat()
38 name = format.stringProperty(QtGui.QTextFormat.ImageName)
39 name = format.stringProperty(QtGui.QTextFormat.ImageName)
39 if name.isEmpty():
40 if name.isEmpty():
40 super(RichIPythonWidget, self)._show_context_menu(pos)
41 super(RichIPythonWidget, self)._show_context_menu(pos)
41 else:
42 else:
42 menu = QtGui.QMenu()
43 menu = QtGui.QMenu()
43
44
44 menu.addAction('Copy Image', lambda: self._copy_image(name))
45 menu.addAction('Copy Image', lambda: self._copy_image(name))
45 menu.addAction('Save Image As...', lambda: self._save_image(name))
46 menu.addAction('Save Image As...', lambda: self._save_image(name))
46 menu.addSeparator()
47 menu.addSeparator()
47
48
48 svg = format.stringProperty(self._svg_text_format_property)
49 svg = format.stringProperty(self._svg_text_format_property)
49 if not svg.isEmpty():
50 if not svg.isEmpty():
50 menu.addSeparator()
51 menu.addSeparator()
51 menu.addAction('Copy SVG', lambda: svg_to_clipboard(svg))
52 menu.addAction('Copy SVG', lambda: svg_to_clipboard(svg))
52 menu.addAction('Save SVG As...',
53 menu.addAction('Save SVG As...',
53 lambda: save_svg(svg, self._control))
54 lambda: save_svg(svg, self._control))
54
55
55 menu.exec_(self._control.mapToGlobal(pos))
56 menu.exec_(self._control.mapToGlobal(pos))
56
57
57 #---------------------------------------------------------------------------
58 #---------------------------------------------------------------------------
58 # 'FrontendWidget' protected interface
59 # 'FrontendWidget' protected interface
59 #---------------------------------------------------------------------------
60 #---------------------------------------------------------------------------
60
61
61 def _process_execute_ok(self, msg):
62 def _process_execute_payload(self, item):
62 """ Reimplemented to handle matplotlib plot payloads.
63 """ Reimplemented to handle matplotlib plot payloads.
63 """
64 """
64 payload = msg['content']['payload']
65 if item['source'] == self._payload_source_plot:
65 for item in payload:
66 if item['format'] == 'svg':
66 if item['source'] == 'IPython.zmq.pylab.backend_payload.add_plot_payload':
67 svg = item['data']
67 if item['format'] == 'svg':
68 try:
68 svg = item['data']
69 image = svg_to_image(svg)
69 try:
70 except ValueError:
70 image = svg_to_image(svg)
71 self._append_plain_text('Received invalid plot data.')
71 except ValueError:
72 self._append_plain_text('Received invalid plot data.')
73 else:
74 format = self._add_image(image)
75 format.setProperty(self._svg_text_format_property, svg)
76 cursor = self._get_end_cursor()
77 cursor.insertBlock()
78 cursor.insertImage(format)
79 cursor.insertBlock()
80 else:
72 else:
81 # Add other plot formats here!
73 format = self._add_image(image)
82 pass
74 format.setProperty(self._svg_text_format_property, svg)
83 elif item['source'] == 'IPython.zmq.zmqshell.ZMQInteractiveShell.edit_magic':
75 cursor = self._get_end_cursor()
84 # TODO: I have implmented the logic for TextMate on the Mac.
76 cursor.insertBlock()
85 # But, we need to allow payload handlers on the non-rich
77 cursor.insertImage(format)
86 # text IPython widget as well. Furthermore, we should probably
78 cursor.insertBlock()
87 # move these handlers to separate methods. But, we need to
79 return True
88 # be very careful to process the payload list in order. Thus,
89 # we will probably need a _handle_payload method of the
90 # base class that dispatches to the separate handler methods
91 # for each payload source. If a particular subclass doesn't
92 # have a handler for a payload source, it should at least
93 # print a nice message.
94 filename = item['filename']
95 line_number = item['line_number']
96 if line_number is None:
97 cmd = 'mate %s' % filename
98 else:
99 cmd = 'mate -l %s %s' % (line_number, filename)
100 os.system(cmd)
101 elif item['source'] == 'IPython.zmq.page.page':
102 # TODO: This is probably a good place to start, but Evan can
103 # add better paging capabilities.
104 self._append_plain_text(item['data'])
105 else:
80 else:
106 # Add other payload types here!
81 # Add other plot formats here!
107 pass
82 return False
108 else:
83 else:
109 super(RichIPythonWidget, self)._process_execute_ok(msg)
84 return super(RichIPythonWidget, self)._process_execute_ok(msg)
110
85
111 #---------------------------------------------------------------------------
86 #---------------------------------------------------------------------------
112 # 'RichIPythonWidget' protected interface
87 # 'RichIPythonWidget' protected interface
113 #---------------------------------------------------------------------------
88 #---------------------------------------------------------------------------
114
89
115 def _add_image(self, image):
90 def _add_image(self, image):
116 """ Adds the specified QImage to the document and returns a
91 """ Adds the specified QImage to the document and returns a
117 QTextImageFormat that references it.
92 QTextImageFormat that references it.
118 """
93 """
119 document = self._control.document()
94 document = self._control.document()
120 name = QtCore.QString.number(image.cacheKey())
95 name = QtCore.QString.number(image.cacheKey())
121 document.addResource(QtGui.QTextDocument.ImageResource,
96 document.addResource(QtGui.QTextDocument.ImageResource,
122 QtCore.QUrl(name), image)
97 QtCore.QUrl(name), image)
123 format = QtGui.QTextImageFormat()
98 format = QtGui.QTextImageFormat()
124 format.setName(name)
99 format.setName(name)
125 return format
100 return format
126
101
127 def _copy_image(self, name):
102 def _copy_image(self, name):
128 """ Copies the ImageResource with 'name' to the clipboard.
103 """ Copies the ImageResource with 'name' to the clipboard.
129 """
104 """
130 image = self._get_image(name)
105 image = self._get_image(name)
131 QtGui.QApplication.clipboard().setImage(image)
106 QtGui.QApplication.clipboard().setImage(image)
132
107
133 def _get_image(self, name):
108 def _get_image(self, name):
134 """ Returns the QImage stored as the ImageResource with 'name'.
109 """ Returns the QImage stored as the ImageResource with 'name'.
135 """
110 """
136 document = self._control.document()
111 document = self._control.document()
137 variant = document.resource(QtGui.QTextDocument.ImageResource,
112 variant = document.resource(QtGui.QTextDocument.ImageResource,
138 QtCore.QUrl(name))
113 QtCore.QUrl(name))
139 return variant.toPyObject()
114 return variant.toPyObject()
140
115
141 def _save_image(self, name, format='PNG'):
116 def _save_image(self, name, format='PNG'):
142 """ Shows a save dialog for the ImageResource with 'name'.
117 """ Shows a save dialog for the ImageResource with 'name'.
143 """
118 """
144 dialog = QtGui.QFileDialog(self._control, 'Save Image')
119 dialog = QtGui.QFileDialog(self._control, 'Save Image')
145 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
120 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
146 dialog.setDefaultSuffix(format.lower())
121 dialog.setDefaultSuffix(format.lower())
147 dialog.setNameFilter('%s file (*.%s)' % (format, format.lower()))
122 dialog.setNameFilter('%s file (*.%s)' % (format, format.lower()))
148 if dialog.exec_():
123 if dialog.exec_():
149 filename = dialog.selectedFiles()[0]
124 filename = dialog.selectedFiles()[0]
150 image = self._get_image(name)
125 image = self._get_image(name)
151 image.save(filename, format)
126 image.save(filename, format)
General Comments 0
You need to be logged in to leave comments. Login now