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