##// END OF EJS Templates
Added a method on IPythonWidget for setting the style to the IPython defaults.
epatters -
Show More
@@ -1,359 +1,393 b''
1 1 """ A FrontendWidget that emulates the interface of the console IPython and
2 2 supports the additional functionality provided by the IPython kernel.
3 3
4 4 TODO: Add support for retrieving the system default editor. Requires code
5 5 paths for Windows (use the registry), Mac OS (use LaunchServices), and
6 6 Linux (use the xdg system).
7 7 """
8 8
9 #-----------------------------------------------------------------------------
10 # Imports
11 #-----------------------------------------------------------------------------
12
9 13 # Standard library imports
10 14 from collections import namedtuple
11 15 from subprocess import Popen
12 16
13 17 # System library imports
14 18 from PyQt4 import QtCore, QtGui
15 19
16 20 # Local imports
17 21 from IPython.core.inputsplitter import IPythonInputSplitter
18 22 from IPython.core.usage import default_banner
19 23 from IPython.utils.traitlets import Bool, Str
20 24 from frontend_widget import FrontendWidget
21 25
22 # The default style sheet: black text on a white background.
23 default_style_sheet = '''
26 #-----------------------------------------------------------------------------
27 # Constants
28 #-----------------------------------------------------------------------------
29
30 # The default light style sheet: black text on a white background.
31 default_light_style_sheet = '''
24 32 .error { color: red; }
25 33 .in-prompt { color: navy; }
26 34 .in-prompt-number { font-weight: bold; }
27 35 .out-prompt { color: darkred; }
28 36 .out-prompt-number { font-weight: bold; }
29 37 '''
30 default_syntax_style = 'default'
38 default_light_syntax_style = 'default'
31 39
32 # A dark style sheet: white text on a black background.
33 dark_style_sheet = '''
40 # The default dark style sheet: white text on a black background.
41 default_dark_style_sheet = '''
34 42 QPlainTextEdit, QTextEdit { background-color: black; color: white }
35 43 QFrame { border: 1px solid grey; }
36 44 .error { color: red; }
37 45 .in-prompt { color: lime; }
38 46 .in-prompt-number { color: lime; font-weight: bold; }
39 47 .out-prompt { color: red; }
40 48 .out-prompt-number { color: red; font-weight: bold; }
41 49 '''
42 dark_syntax_style = 'monokai'
50 default_dark_syntax_style = 'monokai'
43 51
44 52 # Default prompts.
45 53 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
46 54 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
47 55
56 #-----------------------------------------------------------------------------
57 # IPythonWidget class
58 #-----------------------------------------------------------------------------
48 59
49 60 class IPythonWidget(FrontendWidget):
50 61 """ A FrontendWidget for an IPython kernel.
51 62 """
52 63
53 64 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
54 65 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
55 66 # settings.
56 67 custom_edit = Bool(False)
57 68 custom_edit_requested = QtCore.pyqtSignal(object, object)
58 69
59 70 # A command for invoking a system text editor. If the string contains a
60 71 # {filename} format specifier, it will be used. Otherwise, the filename will
61 72 # be appended to the end the command.
62 73 editor = Str('default', config=True)
63 74
64 75 # The editor command to use when a specific line number is requested. The
65 76 # string should contain two format specifiers: {line} and {filename}. If
66 77 # this parameter is not specified, the line number option to the %edit magic
67 78 # will be ignored.
68 79 editor_line = Str(config=True)
69 80
70 81 # A CSS stylesheet. The stylesheet can contain classes for:
71 82 # 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
72 83 # 2. Pygments: .c, .k, .o, etc (see PygmentsHighlighter)
73 84 # 3. IPython: .error, .in-prompt, .out-prompt, etc
74 style_sheet = Str(default_style_sheet, config=True)
85 style_sheet = Str(config=True)
75 86
76 87 # If not empty, use this Pygments style for syntax highlighting. Otherwise,
77 88 # the style sheet is queried for Pygments style information.
78 syntax_style = Str(default_syntax_style, config=True)
89 syntax_style = Str(config=True)
79 90
80 91 # Prompts.
81 92 in_prompt = Str(default_in_prompt, config=True)
82 93 out_prompt = Str(default_out_prompt, config=True)
83 94
84 95 # FrontendWidget protected class variables.
85 96 _input_splitter_class = IPythonInputSplitter
86 97
87 98 # IPythonWidget protected class variables.
88 99 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
89 100 _payload_source_edit = 'IPython.zmq.zmqshell.ZMQInteractiveShell.edit_magic'
90 101 _payload_source_page = 'IPython.zmq.page.page'
91 102
92 103 #---------------------------------------------------------------------------
93 104 # 'object' interface
94 105 #---------------------------------------------------------------------------
95 106
96 107 def __init__(self, *args, **kw):
97 108 super(IPythonWidget, self).__init__(*args, **kw)
98 109
99 110 # IPythonWidget protected variables.
100 111 self._previous_prompt_obj = None
101 112
102 113 # Initialize widget styling.
103 self._style_sheet_changed()
104 self._syntax_style_changed()
114 if self.style_sheet:
115 self._style_sheet_changed()
116 self._syntax_style_changed()
117 else:
118 self.set_default_style()
105 119
106 120 #---------------------------------------------------------------------------
107 121 # 'BaseFrontendMixin' abstract interface
108 122 #---------------------------------------------------------------------------
109 123
110 124 def _handle_complete_reply(self, rep):
111 125 """ Reimplemented to support IPython's improved completion machinery.
112 126 """
113 127 cursor = self._get_cursor()
114 128 if rep['parent_header']['msg_id'] == self._complete_id and \
115 129 cursor.position() == self._complete_pos:
116 130 # The completer tells us what text was actually used for the
117 131 # matching, so we must move that many characters left to apply the
118 132 # completions.
119 133 text = rep['content']['matched_text']
120 134 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
121 135 self._complete_with_items(cursor, rep['content']['matches'])
122 136
123 137 def _handle_history_reply(self, msg):
124 138 """ Implemented to handle history replies, which are only supported by
125 139 the IPython kernel.
126 140 """
127 141 history_dict = msg['content']['history']
128 142 items = [ history_dict[key] for key in sorted(history_dict.keys()) ]
129 143 self._set_history(items)
130 144
131 145 def _handle_prompt_reply(self, msg):
132 146 """ Implemented to handle prompt number replies, which are only
133 147 supported by the IPython kernel.
134 148 """
135 149 content = msg['content']
136 150 self._show_interpreter_prompt(content['prompt_number'],
137 151 content['input_sep'])
138 152
139 153 def _handle_pyout(self, msg):
140 154 """ Reimplemented for IPython-style "display hook".
141 155 """
142 156 if not self._hidden and self._is_from_this_session(msg):
143 157 content = msg['content']
144 158 prompt_number = content['prompt_number']
145 159 self._append_plain_text(content['output_sep'])
146 160 self._append_html(self._make_out_prompt(prompt_number))
147 161 self._append_plain_text(content['data'] + '\n' +
148 162 content['output_sep2'])
149 163
150 164 def _started_channels(self):
151 165 """ Reimplemented to make a history request.
152 166 """
153 167 super(IPythonWidget, self)._started_channels()
154 168 # FIXME: Disabled until history requests are properly implemented.
155 169 #self.kernel_manager.xreq_channel.history(raw=True, output=False)
156 170
157 171 #---------------------------------------------------------------------------
158 172 # 'FrontendWidget' interface
159 173 #---------------------------------------------------------------------------
160 174
161 175 def execute_file(self, path, hidden=False):
162 176 """ Reimplemented to use the 'run' magic.
163 177 """
164 178 self.execute('%%run %s' % path, hidden=hidden)
165 179
166 180 #---------------------------------------------------------------------------
167 181 # 'FrontendWidget' protected interface
168 182 #---------------------------------------------------------------------------
169 183
170 184 def _complete(self):
171 185 """ Reimplemented to support IPython's improved completion machinery.
172 186 """
173 187 # We let the kernel split the input line, so we *always* send an empty
174 188 # text field. Readline-based frontends do get a real text field which
175 189 # they can use.
176 190 text = ''
177 191
178 192 # Send the completion request to the kernel
179 193 self._complete_id = self.kernel_manager.xreq_channel.complete(
180 194 text, # text
181 195 self._get_input_buffer_cursor_line(), # line
182 196 self._get_input_buffer_cursor_column(), # cursor_pos
183 197 self.input_buffer) # block
184 198 self._complete_pos = self._get_cursor().position()
185 199
186 200 def _get_banner(self):
187 201 """ Reimplemented to return IPython's default banner.
188 202 """
189 203 return default_banner + '\n'
190 204
191 205 def _process_execute_error(self, msg):
192 206 """ Reimplemented for IPython-style traceback formatting.
193 207 """
194 208 content = msg['content']
195 209 traceback = '\n'.join(content['traceback']) + '\n'
196 210 if False:
197 211 # FIXME: For now, tracebacks come as plain text, so we can't use
198 212 # the html renderer yet. Once we refactor ultratb to produce
199 213 # properly styled tracebacks, this branch should be the default
200 214 traceback = traceback.replace(' ', '&nbsp;')
201 215 traceback = traceback.replace('\n', '<br/>')
202 216
203 217 ename = content['ename']
204 218 ename_styled = '<span class="error">%s</span>' % ename
205 219 traceback = traceback.replace(ename, ename_styled)
206 220
207 221 self._append_html(traceback)
208 222 else:
209 223 # This is the fallback for now, using plain text with ansi escapes
210 224 self._append_plain_text(traceback)
211 225
212 226 def _process_execute_payload(self, item):
213 227 """ Reimplemented to handle %edit and paging payloads.
214 228 """
215 229 if item['source'] == self._payload_source_edit:
216 230 self._edit(item['filename'], item['line_number'])
217 231 return True
218 232 elif item['source'] == self._payload_source_page:
219 233 self._page(item['data'])
220 234 return True
221 235 else:
222 236 return False
223 237
224 238 def _show_interpreter_prompt(self, number=None, input_sep='\n'):
225 239 """ Reimplemented for IPython-style prompts.
226 240 """
227 241 # If a number was not specified, make a prompt number request.
228 242 if number is None:
229 243 self.kernel_manager.xreq_channel.prompt()
230 244 return
231 245
232 246 # Show a new prompt and save information about it so that it can be
233 247 # updated later if the prompt number turns out to be wrong.
234 248 self._prompt_sep = input_sep
235 249 self._show_prompt(self._make_in_prompt(number), html=True)
236 250 block = self._control.document().lastBlock()
237 251 length = len(self._prompt)
238 252 self._previous_prompt_obj = self._PromptBlock(block, length, number)
239 253
240 254 # Update continuation prompt to reflect (possibly) new prompt length.
241 255 self._set_continuation_prompt(
242 256 self._make_continuation_prompt(self._prompt), html=True)
243 257
244 258 def _show_interpreter_prompt_for_reply(self, msg):
245 259 """ Reimplemented for IPython-style prompts.
246 260 """
247 261 # Update the old prompt number if necessary.
248 262 content = msg['content']
249 263 previous_prompt_number = content['prompt_number']
250 264 if self._previous_prompt_obj and \
251 265 self._previous_prompt_obj.number != previous_prompt_number:
252 266 block = self._previous_prompt_obj.block
253 267
254 268 # Make sure the prompt block has not been erased.
255 269 if block.isValid() and not block.text().isEmpty():
256 270
257 271 # Remove the old prompt and insert a new prompt.
258 272 cursor = QtGui.QTextCursor(block)
259 273 cursor.movePosition(QtGui.QTextCursor.Right,
260 274 QtGui.QTextCursor.KeepAnchor,
261 275 self._previous_prompt_obj.length)
262 276 prompt = self._make_in_prompt(previous_prompt_number)
263 277 self._prompt = self._insert_html_fetching_plain_text(
264 278 cursor, prompt)
265 279
266 280 # When the HTML is inserted, Qt blows away the syntax
267 281 # highlighting for the line, so we need to rehighlight it.
268 282 self._highlighter.rehighlightBlock(cursor.block())
269 283
270 284 self._previous_prompt_obj = None
271 285
272 286 # Show a new prompt with the kernel's estimated prompt number.
273 287 next_prompt = content['next_prompt']
274 288 self._show_interpreter_prompt(next_prompt['prompt_number'],
275 289 next_prompt['input_sep'])
276 290
277 291 #---------------------------------------------------------------------------
292 # 'IPythonWidget' interface
293 #---------------------------------------------------------------------------
294
295 def set_default_style(self, lightbg=True):
296 """ Sets the widget style to the class defaults.
297
298 Parameters:
299 -----------
300 lightbg : bool, optional (default True)
301 Whether to use the default IPython light background or dark
302 background style.
303 """
304 if lightbg:
305 self.style_sheet = default_light_style_sheet
306 self.syntax_style = default_light_syntax_style
307 else:
308 self.style_sheet = default_dark_style_sheet
309 self.syntax_style = default_dark_syntax_style
310
311 #---------------------------------------------------------------------------
278 312 # 'IPythonWidget' protected interface
279 313 #---------------------------------------------------------------------------
280 314
281 315 def _edit(self, filename, line=None):
282 316 """ Opens a Python script for editing.
283 317
284 318 Parameters:
285 319 -----------
286 320 filename : str
287 321 A path to a local system file.
288 322
289 323 line : int, optional
290 324 A line of interest in the file.
291 325 """
292 326 if self.custom_edit:
293 327 self.custom_edit_requested.emit(filename, line)
294 328 elif self.editor == 'default':
295 329 self._append_plain_text('No default editor available.\n')
296 330 else:
297 331 try:
298 332 filename = '"%s"' % filename
299 333 if line and self.editor_line:
300 334 command = self.editor_line.format(filename=filename,
301 335 line=line)
302 336 else:
303 337 try:
304 338 command = self.editor.format()
305 339 except KeyError:
306 340 command = self.editor.format(filename=filename)
307 341 else:
308 342 command += ' ' + filename
309 343 except KeyError:
310 344 self._append_plain_text('Invalid editor command.\n')
311 345 else:
312 346 try:
313 347 Popen(command, shell=True)
314 348 except OSError:
315 349 msg = 'Opening editor with command "%s" failed.\n'
316 350 self._append_plain_text(msg % command)
317 351
318 352 def _make_in_prompt(self, number):
319 353 """ Given a prompt number, returns an HTML In prompt.
320 354 """
321 355 body = self.in_prompt % number
322 356 return '<span class="in-prompt">%s</span>' % body
323 357
324 358 def _make_continuation_prompt(self, prompt):
325 359 """ Given a plain text version of an In prompt, returns an HTML
326 360 continuation prompt.
327 361 """
328 362 end_chars = '...: '
329 363 space_count = len(prompt.lstrip('\n')) - len(end_chars)
330 364 body = '&nbsp;' * space_count + end_chars
331 365 return '<span class="in-prompt">%s</span>' % body
332 366
333 367 def _make_out_prompt(self, number):
334 368 """ Given a prompt number, returns an HTML Out prompt.
335 369 """
336 370 body = self.out_prompt % number
337 371 return '<span class="out-prompt">%s</span>' % body
338 372
339 373 #------ Trait change handlers ---------------------------------------------
340 374
341 375 def _style_sheet_changed(self):
342 376 """ Set the style sheets of the underlying widgets.
343 377 """
344 378 self.setStyleSheet(self.style_sheet)
345 379 self._control.document().setDefaultStyleSheet(self.style_sheet)
346 380 if self._page_control:
347 381 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
348 382
349 383 bg_color = self._control.palette().background().color()
350 384 self._ansi_processor.set_background_color(bg_color)
351 385
352 386 def _syntax_style_changed(self):
353 387 """ Set the style for the syntax highlighter.
354 388 """
355 389 if self.syntax_style:
356 390 self._highlighter.set_style(self.syntax_style)
357 391 else:
358 392 self._highlighter.set_style_sheet(self.style_sheet)
359 393
General Comments 0
You need to be logged in to leave comments. Login now