##// END OF EJS Templates
Refactored ConsoleWidget to encapsulate, rather than inherit from, QPlainTextEdit. This permits a QTextEdit to be substituted for a QPlainTextEdit if desired. It also makes it more clear what is the public interface of ConsoleWidget.
epatters -
Show More
This diff has been collapsed as it changes many lines, (694 lines changed) Show them Hide them
@@ -1,978 +1,1052
1 1 # Standard library imports
2 2 import sys
3 3
4 4 # System library imports
5 5 from PyQt4 import QtCore, QtGui
6 6
7 7 # Local imports
8 8 from ansi_code_processor import QtAnsiCodeProcessor
9 9 from completion_widget import CompletionWidget
10 10
11 11
12 class ConsoleWidget(QtGui.QPlainTextEdit):
12 class ConsoleWidget(QtGui.QWidget):
13 13 """ Base class for console-type widgets. This class is mainly concerned with
14 14 dealing with the prompt, keeping the cursor inside the editing line, and
15 15 handling ANSI escape sequences.
16 16 """
17 17
18 18 # Whether to process ANSI escape codes.
19 19 ansi_codes = True
20 20
21 21 # The maximum number of lines of text before truncation.
22 22 buffer_size = 500
23 23
24 24 # Whether to use a CompletionWidget or plain text output for tab completion.
25 25 gui_completion = True
26 26
27 27 # Whether to override ShortcutEvents for the keybindings defined by this
28 28 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
29 29 # priority (when it has focus) over, e.g., window-level menu shortcuts.
30 30 override_shortcuts = False
31 31
32 # Signals that indicate ConsoleWidget state.
33 copy_available = QtCore.pyqtSignal(bool)
34 redo_available = QtCore.pyqtSignal(bool)
35 undo_available = QtCore.pyqtSignal(bool)
36
32 37 # Protected class variables.
33 38 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
34 39 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
35 40 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
36 41 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
37 42 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
38 43 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
39 44 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
40 45 _shortcuts = set(_ctrl_down_remap.keys() +
41 46 [ QtCore.Qt.Key_C, QtCore.Qt.Key_V ])
42 47
43 48 #---------------------------------------------------------------------------
44 49 # 'QObject' interface
45 50 #---------------------------------------------------------------------------
46 51
47 def __init__(self, parent=None):
48 QtGui.QPlainTextEdit.__init__(self, parent)
52 def __init__(self, kind='plain', parent=None):
53 """ Create a ConsoleWidget.
54
55 Parameters
56 ----------
57 kind : str, optional [default 'plain']
58 The type of text widget to use. Valid values are 'plain', which
59 specifies a QPlainTextEdit, and 'rich', which specifies an
60 QTextEdit.
61
62 parent : QWidget, optional [default None]
63 The parent for this widget.
64 """
65 super(ConsoleWidget, self).__init__(parent)
66
67 # Create the underlying text widget.
68 self._control = self._create_control(kind)
49 69
50 70 # Initialize protected variables. Some variables contain useful state
51 71 # information for subclasses; they should be considered read-only.
52 72 self._ansi_processor = QtAnsiCodeProcessor()
53 self._completion_widget = CompletionWidget(self)
73 self._completion_widget = CompletionWidget(self._control)
54 74 self._continuation_prompt = '> '
55 75 self._continuation_prompt_html = None
56 76 self._executing = False
57 77 self._prompt = ''
58 78 self._prompt_html = None
59 79 self._prompt_pos = 0
60 80 self._reading = False
61 81 self._reading_callback = None
62 82 self._tab_width = 8
63 83
64 # Set a monospaced font.
65 self.reset_font()
66
67 84 # Define a custom context menu.
68 self._context_menu = QtGui.QMenu(self)
69
70 copy_action = QtGui.QAction('Copy', self)
71 copy_action.triggered.connect(self.copy)
72 self.copyAvailable.connect(copy_action.setEnabled)
73 self._context_menu.addAction(copy_action)
85 self._context_menu = self._create_context_menu()
74 86
75 self._paste_action = QtGui.QAction('Paste', self)
76 self._paste_action.triggered.connect(self.paste)
77 self._context_menu.addAction(self._paste_action)
78 self._context_menu.addSeparator()
87 # Set a monospaced font.
88 self.reset_font()
79 89
80 select_all_action = QtGui.QAction('Select All', self)
81 select_all_action.triggered.connect(self.selectAll)
82 self._context_menu.addAction(select_all_action)
83
84 def event(self, event):
85 """ Reimplemented to override shortcuts, if necessary.
86 """
87 # On Mac OS, it is always unnecessary to override shortcuts, hence the
88 # check below. Users should just use the Control key instead of the
89 # Command key.
90 if self.override_shortcuts and \
91 sys.platform != 'darwin' and \
92 event.type() == QtCore.QEvent.ShortcutOverride and \
93 self._control_down(event.modifiers()) and \
94 event.key() in self._shortcuts:
95 event.accept()
96 return True
97 else:
98 return QtGui.QPlainTextEdit.event(self, event)
90 def eventFilter(self, obj, event):
91 """ Reimplemented to ensure a console-like behavior in the underlying
92 text widget.
93 """
94 if obj == self._control:
95 etype = event.type()
96
97 # Override the default context menu with one that does not have
98 # destructive actions.
99 if etype == QtCore.QEvent.ContextMenu:
100 self._context_menu.exec_(event.globalPos())
101 return True
102
103 # Disable moving text by drag and drop.
104 elif etype == QtCore.QEvent.DragMove:
105 return True
106
107 elif etype == QtCore.QEvent.KeyPress:
108 return self._event_filter_keypress(event)
109
110 # On Mac OS, it is always unnecessary to override shortcuts, hence
111 # the check below. Users should just use the Control key instead of
112 # the Command key.
113 elif etype == QtCore.QEvent.ShortcutOverride:
114 if sys.platform != 'darwin' and \
115 self._control_key_down(event.modifiers()) and \
116 event.key() in self._shortcuts:
117 event.accept()
118 return False
119
120 return super(ConsoleWidget, self).eventFilter(obj, event)
99 121
100 122 #---------------------------------------------------------------------------
101 # 'QWidget' interface
123 # 'ConsoleWidget' public interface
102 124 #---------------------------------------------------------------------------
103 125
104 def contextMenuEvent(self, event):
105 """ Reimplemented to create a menu without destructive actions like
106 'Cut' and 'Delete'.
107 """
108 clipboard_empty = QtGui.QApplication.clipboard().text().isEmpty()
109 self._paste_action.setEnabled(not clipboard_empty)
110
111 self._context_menu.exec_(event.globalPos())
112
113 def dragMoveEvent(self, event):
114 """ Reimplemented to disable moving text by drag and drop.
115 """
116 event.ignore()
117
118 def keyPressEvent(self, event):
119 """ Reimplemented to create a console-like interface.
120 """
121 intercepted = False
122 cursor = self.textCursor()
123 position = cursor.position()
124 key = event.key()
125 ctrl_down = self._control_down(event.modifiers())
126 alt_down = event.modifiers() & QtCore.Qt.AltModifier
127 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
128
129 # Even though we have reimplemented 'paste', the C++ level slot is still
130 # called by Qt. So we intercept the key press here.
131 if event.matches(QtGui.QKeySequence.Paste):
132 self.paste()
133 intercepted = True
134
135 elif ctrl_down:
136 if key in self._ctrl_down_remap:
137 ctrl_down = False
138 key = self._ctrl_down_remap[key]
139 event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, key,
140 QtCore.Qt.NoModifier)
141
142 elif key == QtCore.Qt.Key_K:
143 if self._in_buffer(position):
144 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
145 QtGui.QTextCursor.KeepAnchor)
146 cursor.removeSelectedText()
147 intercepted = True
148
149 elif key == QtCore.Qt.Key_X:
150 intercepted = True
151
152 elif key == QtCore.Qt.Key_Y:
153 self.paste()
154 intercepted = True
155
156 elif alt_down:
157 if key == QtCore.Qt.Key_B:
158 self.setTextCursor(self._get_word_start_cursor(position))
159 intercepted = True
160
161 elif key == QtCore.Qt.Key_F:
162 self.setTextCursor(self._get_word_end_cursor(position))
163 intercepted = True
164
165 elif key == QtCore.Qt.Key_Backspace:
166 cursor = self._get_word_start_cursor(position)
167 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
168 cursor.removeSelectedText()
169 intercepted = True
170
171 elif key == QtCore.Qt.Key_D:
172 cursor = self._get_word_end_cursor(position)
173 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
174 cursor.removeSelectedText()
175 intercepted = True
176
177 if self._completion_widget.isVisible():
178 self._completion_widget.keyPressEvent(event)
179 intercepted = event.isAccepted()
180
181 else:
182 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
183 if self._reading:
184 self.appendPlainText('\n')
185 self._reading = False
186 if self._reading_callback:
187 self._reading_callback()
188 elif not self._executing:
189 self.execute(interactive=True)
190 intercepted = True
191
192 elif key == QtCore.Qt.Key_Up:
193 if self._reading or not self._up_pressed():
194 intercepted = True
195 else:
196 prompt_line = self._get_prompt_cursor().blockNumber()
197 intercepted = cursor.blockNumber() <= prompt_line
198
199 elif key == QtCore.Qt.Key_Down:
200 if self._reading or not self._down_pressed():
201 intercepted = True
202 else:
203 end_line = self._get_end_cursor().blockNumber()
204 intercepted = cursor.blockNumber() == end_line
205
206 elif key == QtCore.Qt.Key_Tab:
207 if self._reading:
208 intercepted = False
209 else:
210 intercepted = not self._tab_pressed()
211
212 elif key == QtCore.Qt.Key_Left:
213 intercepted = not self._in_buffer(position - 1)
214
215 elif key == QtCore.Qt.Key_Home:
216 cursor.movePosition(QtGui.QTextCursor.StartOfLine)
217 start_line = cursor.blockNumber()
218 if start_line == self._get_prompt_cursor().blockNumber():
219 start_pos = self._prompt_pos
220 else:
221 start_pos = cursor.position()
222 start_pos += len(self._continuation_prompt)
223 if shift_down and self._in_buffer(position):
224 self._set_selection(position, start_pos)
225 else:
226 self._set_position(start_pos)
227 intercepted = True
228
229 elif key == QtCore.Qt.Key_Backspace and not alt_down:
230
231 # Line deletion (remove continuation prompt)
232 len_prompt = len(self._continuation_prompt)
233 if not self._reading and \
234 cursor.columnNumber() == len_prompt and \
235 position != self._prompt_pos:
236 cursor.setPosition(position - len_prompt,
237 QtGui.QTextCursor.KeepAnchor)
238 cursor.removeSelectedText()
239
240 # Regular backwards deletion
241 else:
242 anchor = cursor.anchor()
243 if anchor == position:
244 intercepted = not self._in_buffer(position - 1)
245 else:
246 intercepted = not self._in_buffer(min(anchor, position))
247
248 elif key == QtCore.Qt.Key_Delete:
249 anchor = cursor.anchor()
250 intercepted = not self._in_buffer(min(anchor, position))
251
252 # Don't move cursor if control is down to allow copy-paste using
253 # the keyboard in any part of the buffer.
254 if not ctrl_down:
255 self._keep_cursor_in_buffer()
256
257 if not intercepted:
258 QtGui.QPlainTextEdit.keyPressEvent(self, event)
259
260 #--------------------------------------------------------------------------
261 # 'QPlainTextEdit' interface
262 #--------------------------------------------------------------------------
263
264 def appendHtml(self, html):
265 """ Reimplemented to not append HTML as a new paragraph, which doesn't
266 make sense for a console widget.
267 """
268 cursor = self._get_end_cursor()
269 self._insert_html(cursor, html)
270
271 def appendPlainText(self, text):
272 """ Reimplemented to not append text as a new paragraph, which doesn't
273 make sense for a console widget. Also, if enabled, handle ANSI
274 codes.
275 """
276 cursor = self._get_end_cursor()
277 if self.ansi_codes:
278 for substring in self._ansi_processor.split_string(text):
279 format = self._ansi_processor.get_format()
280 cursor.insertText(substring, format)
281 else:
282 cursor.insertText(text)
283
284 126 def clear(self, keep_input=False):
285 """ Reimplemented to write a new prompt. If 'keep_input' is set,
127 """ Clear the console, then write a new prompt. If 'keep_input' is set,
286 128 restores the old input buffer when the new prompt is written.
287 129 """
288 QtGui.QPlainTextEdit.clear(self)
130 self._control.clear()
289 131 if keep_input:
290 132 input_buffer = self.input_buffer
291 133 self._show_prompt()
292 134 if keep_input:
293 135 self.input_buffer = input_buffer
294 136
295 def paste(self):
296 """ Reimplemented to ensure that text is pasted in the editing region.
137 def copy(self):
138 """ Copy the current selected text to the clipboard.
297 139 """
298 self._keep_cursor_in_buffer()
299 QtGui.QPlainTextEdit.paste(self)
300
301 def print_(self, printer):
302 """ Reimplemented to work around a bug in PyQt: the C++ level 'print_'
303 slot has the wrong signature.
304 """
305 QtGui.QPlainTextEdit.print_(self, printer)
306
307 #---------------------------------------------------------------------------
308 # 'ConsoleWidget' public interface
309 #---------------------------------------------------------------------------
140 self._control.copy()
310 141
311 142 def execute(self, source=None, hidden=False, interactive=False):
312 143 """ Executes source or the input buffer, possibly prompting for more
313 144 input.
314 145
315 146 Parameters:
316 147 -----------
317 148 source : str, optional
318 149
319 150 The source to execute. If not specified, the input buffer will be
320 151 used. If specified and 'hidden' is False, the input buffer will be
321 152 replaced with the source before execution.
322 153
323 154 hidden : bool, optional (default False)
324 155
325 156 If set, no output will be shown and the prompt will not be modified.
326 157 In other words, it will be completely invisible to the user that
327 158 an execution has occurred.
328 159
329 160 interactive : bool, optional (default False)
330 161
331 162 Whether the console is to treat the source as having been manually
332 163 entered by the user. The effect of this parameter depends on the
333 164 subclass implementation.
334 165
335 166 Raises:
336 167 -------
337 168 RuntimeError
338 169 If incomplete input is given and 'hidden' is True. In this case,
339 170 it not possible to prompt for more input.
340 171
341 172 Returns:
342 173 --------
343 174 A boolean indicating whether the source was executed.
344 175 """
345 176 if not hidden:
346 177 if source is not None:
347 178 self.input_buffer = source
348 179
349 self.appendPlainText('\n')
180 self._append_plain_text('\n')
350 181 self._executing_input_buffer = self.input_buffer
351 182 self._executing = True
352 183 self._prompt_finished()
353 184
354 185 real_source = self.input_buffer if source is None else source
355 186 complete = self._is_complete(real_source, interactive)
356 187 if complete:
357 188 if not hidden:
358 189 # The maximum block count is only in effect during execution.
359 190 # This ensures that _prompt_pos does not become invalid due to
360 191 # text truncation.
361 self.setMaximumBlockCount(self.buffer_size)
192 self._control.document().setMaximumBlockCount(self.buffer_size)
362 193 self._execute(real_source, hidden)
363 194 elif hidden:
364 195 raise RuntimeError('Incomplete noninteractive input: "%s"' % source)
365 196 else:
366 197 self._show_continuation_prompt()
367 198
368 199 return complete
369 200
370 201 def _get_input_buffer(self):
371 202 """ The text that the user has entered entered at the current prompt.
372 203 """
373 204 # If we're executing, the input buffer may not even exist anymore due to
374 205 # the limit imposed by 'buffer_size'. Therefore, we store it.
375 206 if self._executing:
376 207 return self._executing_input_buffer
377 208
378 209 cursor = self._get_end_cursor()
379 210 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
380 211 input_buffer = str(cursor.selection().toPlainText())
381 212
382 213 # Strip out continuation prompts.
383 214 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
384 215
385 216 def _set_input_buffer(self, string):
386 217 """ Replaces the text in the input buffer with 'string'.
387 218 """
388 219 # Remove old text.
389 220 cursor = self._get_end_cursor()
390 221 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
391 222 cursor.removeSelectedText()
392 223
393 224 # Insert new text with continuation prompts.
394 225 lines = string.splitlines(True)
395 226 if lines:
396 self.appendPlainText(lines[0])
227 self._append_plain_text(lines[0])
397 228 for i in xrange(1, len(lines)):
398 229 if self._continuation_prompt_html is None:
399 self.appendPlainText(self._continuation_prompt)
230 self._append_plain_text(self._continuation_prompt)
400 231 else:
401 self.appendHtml(self._continuation_prompt_html)
402 self.appendPlainText(lines[i])
403 self.moveCursor(QtGui.QTextCursor.End)
232 self._append_html(self._continuation_prompt_html)
233 self._append_plain_text(lines[i])
234 self._control.moveCursor(QtGui.QTextCursor.End)
404 235
405 236 input_buffer = property(_get_input_buffer, _set_input_buffer)
406 237
407 238 def _get_input_buffer_cursor_line(self):
408 239 """ The text in the line of the input buffer in which the user's cursor
409 240 rests. Returns a string if there is such a line; otherwise, None.
410 241 """
411 242 if self._executing:
412 243 return None
413 cursor = self.textCursor()
244 cursor = self._control.textCursor()
414 245 if cursor.position() >= self._prompt_pos:
415 246 text = self._get_block_plain_text(cursor.block())
416 247 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
417 248 return text[len(self._prompt):]
418 249 else:
419 250 return text[len(self._continuation_prompt):]
420 251 else:
421 252 return None
422 253
423 254 input_buffer_cursor_line = property(_get_input_buffer_cursor_line)
424 255
425 256 def _get_font(self):
426 257 """ The base font being used by the ConsoleWidget.
427 258 """
428 return self.document().defaultFont()
259 return self._control.document().defaultFont()
429 260
430 261 def _set_font(self, font):
431 262 """ Sets the base font for the ConsoleWidget to the specified QFont.
432 263 """
433 264 font_metrics = QtGui.QFontMetrics(font)
434 self.setTabStopWidth(self.tab_width * font_metrics.width(' '))
265 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
435 266
436 267 self._completion_widget.setFont(font)
437 self.document().setDefaultFont(font)
268 self._control.document().setDefaultFont(font)
438 269
439 270 font = property(_get_font, _set_font)
440 271
272 def paste(self):
273 """ Paste the contents of the clipboard into the input region.
274 """
275 self._keep_cursor_in_buffer()
276 self._control.paste()
277
278 def print_(self, printer):
279 """ Print the contents of the ConsoleWidget to the specified QPrinter.
280 """
281 self._control.print_(printer)
282
283 def redo(self):
284 """ Redo the last operation. If there is no operation to redo, nothing
285 happens.
286 """
287 self._control.redo()
288
441 289 def reset_font(self):
442 290 """ Sets the font to the default fixed-width font for this platform.
443 291 """
444 292 if sys.platform == 'win32':
445 293 name = 'Courier'
446 294 elif sys.platform == 'darwin':
447 295 name = 'Monaco'
448 296 else:
449 297 name = 'Monospace'
450 298 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
451 299 font.setStyleHint(QtGui.QFont.TypeWriter)
452 300 self._set_font(font)
453 301
302 def select_all(self):
303 """ Selects all the text in the buffer.
304 """
305 self._control.selectAll()
306
454 307 def _get_tab_width(self):
455 308 """ The width (in terms of space characters) for tab characters.
456 309 """
457 310 return self._tab_width
458 311
459 312 def _set_tab_width(self, tab_width):
460 313 """ Sets the width (in terms of space characters) for tab characters.
461 314 """
462 315 font_metrics = QtGui.QFontMetrics(self.font)
463 self.setTabStopWidth(tab_width * font_metrics.width(' '))
316 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
464 317
465 318 self._tab_width = tab_width
466 319
467 320 tab_width = property(_get_tab_width, _set_tab_width)
468
321
322 def undo(self):
323 """ Undo the last operation. If there is no operation to undo, nothing
324 happens.
325 """
326 self._control.undo()
327
469 328 #---------------------------------------------------------------------------
470 329 # 'ConsoleWidget' abstract interface
471 330 #---------------------------------------------------------------------------
472 331
473 332 def _is_complete(self, source, interactive):
474 333 """ Returns whether 'source' can be executed. When triggered by an
475 334 Enter/Return key press, 'interactive' is True; otherwise, it is
476 335 False.
477 336 """
478 337 raise NotImplementedError
479 338
480 339 def _execute(self, source, hidden):
481 340 """ Execute 'source'. If 'hidden', do not show any output.
482 341 """
483 342 raise NotImplementedError
484 343
485 344 def _prompt_started_hook(self):
486 345 """ Called immediately after a new prompt is displayed.
487 346 """
488 347 pass
489 348
490 349 def _prompt_finished_hook(self):
491 350 """ Called immediately after a prompt is finished, i.e. when some input
492 351 will be processed and a new prompt displayed.
493 352 """
494 353 pass
495 354
496 355 def _up_pressed(self):
497 356 """ Called when the up key is pressed. Returns whether to continue
498 357 processing the event.
499 358 """
500 359 return True
501 360
502 361 def _down_pressed(self):
503 362 """ Called when the down key is pressed. Returns whether to continue
504 363 processing the event.
505 364 """
506 365 return True
507 366
508 367 def _tab_pressed(self):
509 368 """ Called when the tab key is pressed. Returns whether to continue
510 369 processing the event.
511 370 """
512 371 return False
513 372
514 373 #--------------------------------------------------------------------------
515 374 # 'ConsoleWidget' protected interface
516 375 #--------------------------------------------------------------------------
517 376
377 def _append_html(self, html):
378 """ Appends html at the end of the console buffer.
379 """
380 cursor = self._get_end_cursor()
381 self._insert_html(cursor, html)
382
518 383 def _append_html_fetching_plain_text(self, html):
519 384 """ Appends 'html', then returns the plain text version of it.
520 385 """
521 386 anchor = self._get_end_cursor().position()
522 self.appendHtml(html)
387 self._append_html(html)
523 388 cursor = self._get_end_cursor()
524 389 cursor.setPosition(anchor, QtGui.QTextCursor.KeepAnchor)
525 390 return str(cursor.selection().toPlainText())
526 391
392 def _append_plain_text(self, text):
393 """ Appends plain text at the end of the console buffer, processing
394 ANSI codes if enabled.
395 """
396 cursor = self._get_end_cursor()
397 if self.ansi_codes:
398 for substring in self._ansi_processor.split_string(text):
399 format = self._ansi_processor.get_format()
400 cursor.insertText(substring, format)
401 else:
402 cursor.insertText(text)
403
527 404 def _append_plain_text_keeping_prompt(self, text):
528 405 """ Writes 'text' after the current prompt, then restores the old prompt
529 406 with its old input buffer.
530 407 """
531 408 input_buffer = self.input_buffer
532 self.appendPlainText('\n')
409 self._append_plain_text('\n')
533 410 self._prompt_finished()
534 411
535 self.appendPlainText(text)
412 self._append_plain_text(text)
536 413 self._show_prompt()
537 414 self.input_buffer = input_buffer
538 415
539 def _control_down(self, modifiers):
416 def _complete_with_items(self, cursor, items):
417 """ Performs completion with 'items' at the specified cursor location.
418 """
419 if len(items) == 1:
420 cursor.setPosition(self._control.textCursor().position(),
421 QtGui.QTextCursor.KeepAnchor)
422 cursor.insertText(items[0])
423 elif len(items) > 1:
424 if self.gui_completion:
425 self._completion_widget.show_items(cursor, items)
426 else:
427 text = self._format_as_columns(items)
428 self._append_plain_text_keeping_prompt(text)
429
430 def _control_key_down(self, modifiers):
540 431 """ Given a KeyboardModifiers flags object, return whether the Control
541 432 key is down (on Mac OS, treat the Command key as a synonym for
542 433 Control).
543 434 """
544 435 down = bool(modifiers & QtCore.Qt.ControlModifier)
545 436
546 437 # Note: on Mac OS, ControlModifier corresponds to the Command key while
547 438 # MetaModifier corresponds to the Control key.
548 439 if sys.platform == 'darwin':
549 440 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
550 441
551 442 return down
552
553 def _complete_with_items(self, cursor, items):
554 """ Performs completion with 'items' at the specified cursor location.
443
444 def _create_context_menu(self):
445 """ Creates a context menu for the underlying text widget.
555 446 """
556 if len(items) == 1:
557 cursor.setPosition(self.textCursor().position(),
558 QtGui.QTextCursor.KeepAnchor)
559 cursor.insertText(items[0])
560 elif len(items) > 1:
561 if self.gui_completion:
562 self._completion_widget.show_items(cursor, items)
563 else:
564 text = self._format_as_columns(items)
565 self._append_plain_text_keeping_prompt(text)
447 menu = QtGui.QMenu(self)
448 clipboard = QtGui.QApplication.clipboard()
449
450 copy_action = QtGui.QAction('Copy', self)
451 copy_action.triggered.connect(self.copy)
452 self.copy_available.connect(copy_action.setEnabled)
453 menu.addAction(copy_action)
454
455 paste_action = QtGui.QAction('Paste', self)
456 paste_action.triggered.connect(self.paste)
457 clipboard.dataChanged.connect(
458 lambda: paste_action.setEnabled(not clipboard.text().isEmpty()))
459 menu.addAction(paste_action)
460 menu.addSeparator()
461
462 select_all_action = QtGui.QAction('Select All', self)
463 select_all_action.triggered.connect(self.select_all)
464 menu.addAction(select_all_action)
465
466 return menu
467
468 def _create_control(self, kind):
469 """ Creates and sets the underlying text widget.
470 """
471 layout = QtGui.QVBoxLayout(self)
472 layout.setMargin(0)
473 if kind == 'plain':
474 control = QtGui.QPlainTextEdit()
475 elif kind == 'rich':
476 control = QtGui.QTextEdit()
477 else:
478 raise ValueError("Kind %s unknown." % repr(kind))
479 layout.addWidget(control)
480
481 control.installEventFilter(self)
482 control.copyAvailable.connect(self.copy_available)
483 control.redoAvailable.connect(self.redo_available)
484 control.undoAvailable.connect(self.undo_available)
485
486 return control
487
488 def _event_filter_keypress(self, event):
489 """ Filter key events for the underlying text widget to create a
490 console-like interface.
491 """
492 intercepted = False
493 replaced_event = None
494 cursor = self._control.textCursor()
495 position = cursor.position()
496 key = event.key()
497 ctrl_down = self._control_key_down(event.modifiers())
498 alt_down = event.modifiers() & QtCore.Qt.AltModifier
499 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
500
501 # Even though we have reimplemented 'paste', the C++ level slot is still
502 # called by Qt. So we intercept the key press here.
503 if event.matches(QtGui.QKeySequence.Paste):
504 self.paste()
505 intercepted = True
506
507 elif ctrl_down:
508 if key in self._ctrl_down_remap:
509 ctrl_down = False
510 key = self._ctrl_down_remap[key]
511 replaced_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, key,
512 QtCore.Qt.NoModifier)
513
514 elif key == QtCore.Qt.Key_K:
515 if self._in_buffer(position):
516 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
517 QtGui.QTextCursor.KeepAnchor)
518 cursor.removeSelectedText()
519 intercepted = True
520
521 elif key == QtCore.Qt.Key_X:
522 intercepted = True
523
524 elif key == QtCore.Qt.Key_Y:
525 self.paste()
526 intercepted = True
527
528 elif alt_down:
529 if key == QtCore.Qt.Key_B:
530 self._set_cursor(self._get_word_start_cursor(position))
531 intercepted = True
532
533 elif key == QtCore.Qt.Key_F:
534 self._set_cursor(self._get_word_end_cursor(position))
535 intercepted = True
536
537 elif key == QtCore.Qt.Key_Backspace:
538 cursor = self._get_word_start_cursor(position)
539 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
540 cursor.removeSelectedText()
541 intercepted = True
542
543 elif key == QtCore.Qt.Key_D:
544 cursor = self._get_word_end_cursor(position)
545 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
546 cursor.removeSelectedText()
547 intercepted = True
548
549 if self._completion_widget.isVisible():
550 self._completion_widget.keyPressEvent(event)
551 intercepted = event.isAccepted()
552
553 else:
554 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
555 if self._reading:
556 self._append_plain_text('\n')
557 self._reading = False
558 if self._reading_callback:
559 self._reading_callback()
560 elif not self._executing:
561 self.execute(interactive=True)
562 intercepted = True
563
564 elif key == QtCore.Qt.Key_Up:
565 if self._reading or not self._up_pressed():
566 intercepted = True
567 else:
568 prompt_line = self._get_prompt_cursor().blockNumber()
569 intercepted = cursor.blockNumber() <= prompt_line
570
571 elif key == QtCore.Qt.Key_Down:
572 if self._reading or not self._down_pressed():
573 intercepted = True
574 else:
575 end_line = self._get_end_cursor().blockNumber()
576 intercepted = cursor.blockNumber() == end_line
577
578 elif key == QtCore.Qt.Key_Tab:
579 if self._reading:
580 intercepted = False
581 else:
582 intercepted = not self._tab_pressed()
583
584 elif key == QtCore.Qt.Key_Left:
585 intercepted = not self._in_buffer(position - 1)
586
587 elif key == QtCore.Qt.Key_Home:
588 cursor.movePosition(QtGui.QTextCursor.StartOfLine)
589 start_line = cursor.blockNumber()
590 if start_line == self._get_prompt_cursor().blockNumber():
591 start_pos = self._prompt_pos
592 else:
593 start_pos = cursor.position()
594 start_pos += len(self._continuation_prompt)
595 if shift_down and self._in_buffer(position):
596 self._set_selection(position, start_pos)
597 else:
598 self._set_position(start_pos)
599 intercepted = True
600
601 elif key == QtCore.Qt.Key_Backspace and not alt_down:
602
603 # Line deletion (remove continuation prompt)
604 len_prompt = len(self._continuation_prompt)
605 if not self._reading and \
606 cursor.columnNumber() == len_prompt and \
607 position != self._prompt_pos:
608 cursor.setPosition(position - len_prompt,
609 QtGui.QTextCursor.KeepAnchor)
610 cursor.removeSelectedText()
611
612 # Regular backwards deletion
613 else:
614 anchor = cursor.anchor()
615 if anchor == position:
616 intercepted = not self._in_buffer(position - 1)
617 else:
618 intercepted = not self._in_buffer(min(anchor, position))
619
620 elif key == QtCore.Qt.Key_Delete:
621 anchor = cursor.anchor()
622 intercepted = not self._in_buffer(min(anchor, position))
623
624 # Don't move cursor if control is down to allow copy-paste using
625 # the keyboard in any part of the buffer.
626 if not ctrl_down:
627 self._keep_cursor_in_buffer()
628
629 if not intercepted and replaced_event:
630 QtGui.qApp.sendEvent(self._control, replaced_event)
631 return intercepted
566 632
567 633 def _format_as_columns(self, items, separator=' '):
568 634 """ Transform a list of strings into a single string with columns.
569 635
570 636 Parameters
571 637 ----------
572 638 items : sequence of strings
573 639 The strings to process.
574 640
575 641 separator : str, optional [default is two spaces]
576 642 The string that separates columns.
577 643
578 644 Returns
579 645 -------
580 646 The formatted string.
581 647 """
582 648 # Note: this code is adapted from columnize 0.3.2.
583 649 # See http://code.google.com/p/pycolumnize/
584 650
585 651 font_metrics = QtGui.QFontMetrics(self.font)
586 652 displaywidth = max(5, (self.width() / font_metrics.width(' ')) - 1)
587 653
588 654 # Some degenerate cases.
589 655 size = len(items)
590 656 if size == 0:
591 657 return '\n'
592 658 elif size == 1:
593 659 return '%s\n' % str(items[0])
594 660
595 661 # Try every row count from 1 upwards
596 662 array_index = lambda nrows, row, col: nrows*col + row
597 663 for nrows in range(1, size):
598 664 ncols = (size + nrows - 1) // nrows
599 665 colwidths = []
600 666 totwidth = -len(separator)
601 667 for col in range(ncols):
602 668 # Get max column width for this column
603 669 colwidth = 0
604 670 for row in range(nrows):
605 671 i = array_index(nrows, row, col)
606 672 if i >= size: break
607 673 x = items[i]
608 674 colwidth = max(colwidth, len(x))
609 675 colwidths.append(colwidth)
610 676 totwidth += colwidth + len(separator)
611 677 if totwidth > displaywidth:
612 678 break
613 679 if totwidth <= displaywidth:
614 680 break
615 681
616 682 # The smallest number of rows computed and the max widths for each
617 683 # column has been obtained. Now we just have to format each of the rows.
618 684 string = ''
619 685 for row in range(nrows):
620 686 texts = []
621 687 for col in range(ncols):
622 688 i = row + nrows*col
623 689 if i >= size:
624 690 texts.append('')
625 691 else:
626 692 texts.append(items[i])
627 693 while texts and not texts[-1]:
628 694 del texts[-1]
629 695 for col in range(len(texts)):
630 696 texts[col] = texts[col].ljust(colwidths[col])
631 697 string += '%s\n' % str(separator.join(texts))
632 698 return string
633 699
634 700 def _get_block_plain_text(self, block):
635 701 """ Given a QTextBlock, return its unformatted text.
636 702 """
637 703 cursor = QtGui.QTextCursor(block)
638 704 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
639 705 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
640 706 QtGui.QTextCursor.KeepAnchor)
641 707 return str(cursor.selection().toPlainText())
708
709 def _get_cursor(self):
710 """ Convenience method that returns a cursor for the current position.
711 """
712 return self._control.textCursor()
642 713
643 714 def _get_end_cursor(self):
644 715 """ Convenience method that returns a cursor for the last character.
645 716 """
646 cursor = self.textCursor()
717 cursor = self._control.textCursor()
647 718 cursor.movePosition(QtGui.QTextCursor.End)
648 719 return cursor
649 720
650 721 def _get_prompt_cursor(self):
651 722 """ Convenience method that returns a cursor for the prompt position.
652 723 """
653 cursor = self.textCursor()
724 cursor = self._control.textCursor()
654 725 cursor.setPosition(self._prompt_pos)
655 726 return cursor
656 727
657 728 def _get_selection_cursor(self, start, end):
658 729 """ Convenience method that returns a cursor with text selected between
659 730 the positions 'start' and 'end'.
660 731 """
661 cursor = self.textCursor()
732 cursor = self._control.textCursor()
662 733 cursor.setPosition(start)
663 734 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
664 735 return cursor
665 736
666 737 def _get_word_start_cursor(self, position):
667 738 """ Find the start of the word to the left the given position. If a
668 739 sequence of non-word characters precedes the first word, skip over
669 740 them. (This emulates the behavior of bash, emacs, etc.)
670 741 """
671 document = self.document()
742 document = self._control.document()
672 743 position -= 1
673 744 while self._in_buffer(position) and \
674 745 not document.characterAt(position).isLetterOrNumber():
675 746 position -= 1
676 747 while self._in_buffer(position) and \
677 748 document.characterAt(position).isLetterOrNumber():
678 749 position -= 1
679 cursor = self.textCursor()
750 cursor = self._control.textCursor()
680 751 cursor.setPosition(position + 1)
681 752 return cursor
682 753
683 754 def _get_word_end_cursor(self, position):
684 755 """ Find the end of the word to the right the given position. If a
685 756 sequence of non-word characters precedes the first word, skip over
686 757 them. (This emulates the behavior of bash, emacs, etc.)
687 758 """
688 document = self.document()
759 document = self._control.document()
689 760 end = self._get_end_cursor().position()
690 761 while position < end and \
691 762 not document.characterAt(position).isLetterOrNumber():
692 763 position += 1
693 764 while position < end and \
694 765 document.characterAt(position).isLetterOrNumber():
695 766 position += 1
696 cursor = self.textCursor()
767 cursor = self._control.textCursor()
697 768 cursor.setPosition(position)
698 769 return cursor
699 770
700 771 def _insert_html(self, cursor, html):
701 772 """ Insert HTML using the specified cursor in such a way that future
702 773 formatting is unaffected.
703 774 """
704 775 cursor.insertHtml(html)
705 776
706 777 # After inserting HTML, the text document "remembers" the current
707 778 # formatting, which means that subsequent calls adding plain text
708 779 # will result in similar formatting, a behavior that we do not want. To
709 780 # prevent this, we make sure that the last character has no formatting.
710 781 cursor.movePosition(QtGui.QTextCursor.Left,
711 782 QtGui.QTextCursor.KeepAnchor)
712 783 if cursor.selection().toPlainText().trimmed().isEmpty():
713 784 # If the last character is whitespace, it doesn't matter how it's
714 785 # formatted, so just clear the formatting.
715 786 cursor.setCharFormat(QtGui.QTextCharFormat())
716 787 else:
717 788 # Otherwise, add an unformatted space.
718 789 cursor.movePosition(QtGui.QTextCursor.Right)
719 790 cursor.insertText(' ', QtGui.QTextCharFormat())
720 791
792 def _in_buffer(self, position):
793 """ Returns whether the given position is inside the editing region.
794 """
795 return position >= self._prompt_pos
796
797 def _keep_cursor_in_buffer(self):
798 """ Ensures that the cursor is inside the editing region. Returns
799 whether the cursor was moved.
800 """
801 cursor = self._control.textCursor()
802 if cursor.position() < self._prompt_pos:
803 cursor.movePosition(QtGui.QTextCursor.End)
804 self._control.setTextCursor(cursor)
805 return True
806 else:
807 return False
808
721 809 def _prompt_started(self):
722 810 """ Called immediately after a new prompt is displayed.
723 811 """
724 812 # Temporarily disable the maximum block count to permit undo/redo and
725 813 # to ensure that the prompt position does not change due to truncation.
726 self.setMaximumBlockCount(0)
727 self.setUndoRedoEnabled(True)
814 self._control.document().setMaximumBlockCount(0)
815 self._control.setUndoRedoEnabled(True)
728 816
729 self.setReadOnly(False)
730 self.moveCursor(QtGui.QTextCursor.End)
731 self.centerCursor()
817 self._control.setReadOnly(False)
818 self._control.moveCursor(QtGui.QTextCursor.End)
732 819
733 820 self._executing = False
734 821 self._prompt_started_hook()
735 822
736 823 def _prompt_finished(self):
737 824 """ Called immediately after a prompt is finished, i.e. when some input
738 825 will be processed and a new prompt displayed.
739 826 """
740 self.setUndoRedoEnabled(False)
741 self.setReadOnly(True)
827 self._control.setUndoRedoEnabled(False)
828 self._control.setReadOnly(True)
742 829 self._prompt_finished_hook()
743 830
744 831 def _readline(self, prompt='', callback=None):
745 832 """ Reads one line of input from the user.
746 833
747 834 Parameters
748 835 ----------
749 836 prompt : str, optional
750 837 The prompt to print before reading the line.
751 838
752 839 callback : callable, optional
753 840 A callback to execute with the read line. If not specified, input is
754 841 read *synchronously* and this method does not return until it has
755 842 been read.
756 843
757 844 Returns
758 845 -------
759 846 If a callback is specified, returns nothing. Otherwise, returns the
760 847 input string with the trailing newline stripped.
761 848 """
762 849 if self._reading:
763 850 raise RuntimeError('Cannot read a line. Widget is already reading.')
764 851
765 852 if not callback and not self.isVisible():
766 853 # If the user cannot see the widget, this function cannot return.
767 854 raise RuntimeError('Cannot synchronously read a line if the widget'
768 855 'is not visible!')
769 856
770 857 self._reading = True
771 858 self._show_prompt(prompt, newline=False)
772 859
773 860 if callback is None:
774 861 self._reading_callback = None
775 862 while self._reading:
776 863 QtCore.QCoreApplication.processEvents()
777 864 return self.input_buffer.rstrip('\n')
778 865
779 866 else:
780 867 self._reading_callback = lambda: \
781 868 callback(self.input_buffer.rstrip('\n'))
782 869
783 870 def _reset(self):
784 871 """ Clears the console and resets internal state variables.
785 872 """
786 QtGui.QPlainTextEdit.clear(self)
873 self._control.clear()
787 874 self._executing = self._reading = False
788 875
789 876 def _set_continuation_prompt(self, prompt, html=False):
790 877 """ Sets the continuation prompt.
791 878
792 879 Parameters
793 880 ----------
794 881 prompt : str
795 882 The prompt to show when more input is needed.
796 883
797 884 html : bool, optional (default False)
798 885 If set, the prompt will be inserted as formatted HTML. Otherwise,
799 886 the prompt will be treated as plain text, though ANSI color codes
800 887 will be handled.
801 888 """
802 889 if html:
803 890 self._continuation_prompt_html = prompt
804 891 else:
805 892 self._continuation_prompt = prompt
806 893 self._continuation_prompt_html = None
894
895 def _set_cursor(self, cursor):
896 """ Convenience method to set the current cursor.
897 """
898 self._control.setTextCursor(cursor)
807 899
808 900 def _set_position(self, position):
809 901 """ Convenience method to set the position of the cursor.
810 902 """
811 cursor = self.textCursor()
903 cursor = self._control.textCursor()
812 904 cursor.setPosition(position)
813 self.setTextCursor(cursor)
905 self._control.setTextCursor(cursor)
814 906
815 907 def _set_selection(self, start, end):
816 908 """ Convenience method to set the current selected text.
817 909 """
818 self.setTextCursor(self._get_selection_cursor(start, end))
910 self._control.setTextCursor(self._get_selection_cursor(start, end))
819 911
820 912 def _show_prompt(self, prompt=None, html=False, newline=True):
821 913 """ Writes a new prompt at the end of the buffer.
822 914
823 915 Parameters
824 916 ----------
825 917 prompt : str, optional
826 918 The prompt to show. If not specified, the previous prompt is used.
827 919
828 920 html : bool, optional (default False)
829 921 Only relevant when a prompt is specified. If set, the prompt will
830 922 be inserted as formatted HTML. Otherwise, the prompt will be treated
831 923 as plain text, though ANSI color codes will be handled.
832 924
833 925 newline : bool, optional (default True)
834 926 If set, a new line will be written before showing the prompt if
835 927 there is not already a newline at the end of the buffer.
836 928 """
837 929 # Insert a preliminary newline, if necessary.
838 930 if newline:
839 931 cursor = self._get_end_cursor()
840 932 if cursor.position() > 0:
841 933 cursor.movePosition(QtGui.QTextCursor.Left,
842 934 QtGui.QTextCursor.KeepAnchor)
843 935 if str(cursor.selection().toPlainText()) != '\n':
844 self.appendPlainText('\n')
936 self._append_plain_text('\n')
845 937
846 938 # Write the prompt.
847 939 if prompt is None:
848 940 if self._prompt_html is None:
849 self.appendPlainText(self._prompt)
941 self._append_plain_text(self._prompt)
850 942 else:
851 self.appendHtml(self._prompt_html)
943 self._append_html(self._prompt_html)
852 944 else:
853 945 if html:
854 946 self._prompt = self._append_html_fetching_plain_text(prompt)
855 947 self._prompt_html = prompt
856 948 else:
857 self.appendPlainText(prompt)
949 self._append_plain_text(prompt)
858 950 self._prompt = prompt
859 951 self._prompt_html = None
860 952
861 953 self._prompt_pos = self._get_end_cursor().position()
862 954 self._prompt_started()
863 955
864 956 def _show_continuation_prompt(self):
865 957 """ Writes a new continuation prompt at the end of the buffer.
866 958 """
867 959 if self._continuation_prompt_html is None:
868 self.appendPlainText(self._continuation_prompt)
960 self._append_plain_text(self._continuation_prompt)
869 961 else:
870 962 self._continuation_prompt = self._append_html_fetching_plain_text(
871 963 self._continuation_prompt_html)
872 964
873 965 self._prompt_started()
874 966
875 def _in_buffer(self, position):
876 """ Returns whether the given position is inside the editing region.
877 """
878 return position >= self._prompt_pos
879
880 def _keep_cursor_in_buffer(self):
881 """ Ensures that the cursor is inside the editing region. Returns
882 whether the cursor was moved.
883 """
884 cursor = self.textCursor()
885 if cursor.position() < self._prompt_pos:
886 cursor.movePosition(QtGui.QTextCursor.End)
887 self.setTextCursor(cursor)
888 return True
889 else:
890 return False
891
892 967
893 968 class HistoryConsoleWidget(ConsoleWidget):
894 969 """ A ConsoleWidget that keeps a history of the commands that have been
895 970 executed.
896 971 """
897 972
898 973 #---------------------------------------------------------------------------
899 # 'QObject' interface
974 # 'object' interface
900 975 #---------------------------------------------------------------------------
901 976
902 def __init__(self, parent=None):
903 super(HistoryConsoleWidget, self).__init__(parent)
904
977 def __init__(self, *args, **kw):
978 super(HistoryConsoleWidget, self).__init__(*args, **kw)
905 979 self._history = []
906 980 self._history_index = 0
907 981
908 982 #---------------------------------------------------------------------------
909 983 # 'ConsoleWidget' public interface
910 984 #---------------------------------------------------------------------------
911 985
912 986 def execute(self, source=None, hidden=False, interactive=False):
913 987 """ Reimplemented to the store history.
914 988 """
915 989 if not hidden:
916 990 history = self.input_buffer if source is None else source
917 991
918 992 executed = super(HistoryConsoleWidget, self).execute(
919 993 source, hidden, interactive)
920 994
921 995 if executed and not hidden:
922 996 self._history.append(history.rstrip())
923 997 self._history_index = len(self._history)
924 998
925 999 return executed
926 1000
927 1001 #---------------------------------------------------------------------------
928 1002 # 'ConsoleWidget' abstract interface
929 1003 #---------------------------------------------------------------------------
930 1004
931 1005 def _up_pressed(self):
932 1006 """ Called when the up key is pressed. Returns whether to continue
933 1007 processing the event.
934 1008 """
935 1009 prompt_cursor = self._get_prompt_cursor()
936 if self.textCursor().blockNumber() == prompt_cursor.blockNumber():
1010 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
937 1011 self.history_previous()
938 1012
939 1013 # Go to the first line of prompt for seemless history scrolling.
940 1014 cursor = self._get_prompt_cursor()
941 1015 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
942 self.setTextCursor(cursor)
1016 self._set_cursor(cursor)
943 1017
944 1018 return False
945 1019 return True
946 1020
947 1021 def _down_pressed(self):
948 1022 """ Called when the down key is pressed. Returns whether to continue
949 1023 processing the event.
950 1024 """
951 1025 end_cursor = self._get_end_cursor()
952 if self.textCursor().blockNumber() == end_cursor.blockNumber():
1026 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
953 1027 self.history_next()
954 1028 return False
955 1029 return True
956 1030
957 1031 #---------------------------------------------------------------------------
958 1032 # 'HistoryConsoleWidget' interface
959 1033 #---------------------------------------------------------------------------
960 1034
961 1035 def history_previous(self):
962 1036 """ If possible, set the input buffer to the previous item in the
963 1037 history.
964 1038 """
965 1039 if self._history_index > 0:
966 1040 self._history_index -= 1
967 1041 self.input_buffer = self._history[self._history_index]
968 1042
969 1043 def history_next(self):
970 1044 """ Set the input buffer to the next item in the history, or a blank
971 1045 line if there is no subsequent item.
972 1046 """
973 1047 if self._history_index < len(self._history):
974 1048 self._history_index += 1
975 1049 if self._history_index < len(self._history):
976 1050 self.input_buffer = self._history[self._history_index]
977 1051 else:
978 1052 self.input_buffer = ''
@@ -1,382 +1,384
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 call_tip_widget import CallTipWidget
13 13 from completion_lexer import CompletionLexer
14 14 from console_widget import HistoryConsoleWidget
15 15 from pygments_highlighter import PygmentsHighlighter
16 16
17 17
18 18 class FrontendHighlighter(PygmentsHighlighter):
19 19 """ A PygmentsHighlighter that can be turned on and off and that ignores
20 20 prompts.
21 21 """
22 22
23 23 def __init__(self, frontend):
24 super(FrontendHighlighter, self).__init__(frontend.document())
24 super(FrontendHighlighter, self).__init__(frontend._control.document())
25 25 self._current_offset = 0
26 26 self._frontend = frontend
27 27 self.highlighting_on = False
28 28
29 29 def highlightBlock(self, qstring):
30 30 """ Highlight a block of text. Reimplemented to highlight selectively.
31 31 """
32 32 if not self.highlighting_on:
33 33 return
34 34
35 35 # The input to this function is unicode string that may contain
36 36 # paragraph break characters, non-breaking spaces, etc. Here we acquire
37 37 # the string as plain text so we can compare it.
38 38 current_block = self.currentBlock()
39 39 string = self._frontend._get_block_plain_text(current_block)
40 40
41 41 # Decide whether to check for the regular or continuation prompt.
42 42 if current_block.contains(self._frontend._prompt_pos):
43 43 prompt = self._frontend._prompt
44 44 else:
45 45 prompt = self._frontend._continuation_prompt
46 46
47 47 # Don't highlight the part of the string that contains the prompt.
48 48 if string.startswith(prompt):
49 49 self._current_offset = len(prompt)
50 50 qstring.remove(0, len(prompt))
51 51 else:
52 52 self._current_offset = 0
53 53
54 54 PygmentsHighlighter.highlightBlock(self, qstring)
55 55
56 56 def setFormat(self, start, count, format):
57 57 """ Reimplemented to highlight selectively.
58 58 """
59 59 start += self._current_offset
60 60 PygmentsHighlighter.setFormat(self, start, count, format)
61 61
62 62
63 63 class FrontendWidget(HistoryConsoleWidget):
64 64 """ A Qt frontend for a generic Python kernel.
65 65 """
66 66
67 67 # Emitted when an 'execute_reply' is received from the kernel.
68 68 executed = QtCore.pyqtSignal(object)
69 69
70 70 #---------------------------------------------------------------------------
71 # 'QObject' interface
71 # 'object' interface
72 72 #---------------------------------------------------------------------------
73 73
74 def __init__(self, parent=None):
75 super(FrontendWidget, self).__init__(parent)
74 def __init__(self, *args, **kw):
75 super(FrontendWidget, self).__init__(*args, **kw)
76 76
77 77 # FrontendWidget protected variables.
78 self._call_tip_widget = CallTipWidget(self)
78 self._call_tip_widget = CallTipWidget(self._control)
79 79 self._completion_lexer = CompletionLexer(PythonLexer())
80 80 self._hidden = True
81 81 self._highlighter = FrontendHighlighter(self)
82 82 self._input_splitter = InputSplitter(input_mode='replace')
83 83 self._kernel_manager = None
84 84
85 85 # Configure the ConsoleWidget.
86 86 self.tab_width = 4
87 87 self._set_continuation_prompt('... ')
88 88
89 self.document().contentsChange.connect(self._document_contents_change)
89 # Connect signal handlers.
90 document = self._control.document()
91 document.contentsChange.connect(self._document_contents_change)
90 92
91 93 #---------------------------------------------------------------------------
92 94 # 'QWidget' interface
93 95 #---------------------------------------------------------------------------
94 96
95 97 def focusOutEvent(self, event):
96 98 """ Reimplemented to hide calltips.
97 99 """
98 100 self._call_tip_widget.hide()
99 101 super(FrontendWidget, self).focusOutEvent(event)
100 102
101 103 def keyPressEvent(self, event):
102 104 """ Reimplemented to allow calltips to process events and to send
103 105 signals to the kernel.
104 106 """
105 107 if self._executing and event.key() == QtCore.Qt.Key_C and \
106 108 self._control_down(event.modifiers()):
107 109 self._interrupt_kernel()
108 110 else:
109 111 if self._call_tip_widget.isVisible():
110 112 self._call_tip_widget.keyPressEvent(event)
111 113 super(FrontendWidget, self).keyPressEvent(event)
112 114
113 115 #---------------------------------------------------------------------------
114 116 # 'ConsoleWidget' abstract interface
115 117 #---------------------------------------------------------------------------
116 118
117 119 def _is_complete(self, source, interactive):
118 120 """ Returns whether 'source' can be completely processed and a new
119 121 prompt created. When triggered by an Enter/Return key press,
120 122 'interactive' is True; otherwise, it is False.
121 123 """
122 124 complete = self._input_splitter.push(source.expandtabs(4))
123 125 if interactive:
124 126 complete = not self._input_splitter.push_accepts_more()
125 127 return complete
126 128
127 129 def _execute(self, source, hidden):
128 130 """ Execute 'source'. If 'hidden', do not show any output.
129 131 """
130 132 self.kernel_manager.xreq_channel.execute(source)
131 133 self._hidden = hidden
132 134
133 135 def _prompt_started_hook(self):
134 136 """ Called immediately after a new prompt is displayed.
135 137 """
136 138 if not self._reading:
137 139 self._highlighter.highlighting_on = True
138 140
139 141 # Auto-indent if this is a continuation prompt.
140 142 if self._get_prompt_cursor().blockNumber() != \
141 143 self._get_end_cursor().blockNumber():
142 144 spaces = self._input_splitter.indent_spaces
143 self.appendPlainText('\t' * (spaces / self.tab_width))
144 self.appendPlainText(' ' * (spaces % self.tab_width))
145 self._append_plain_text('\t' * (spaces / self.tab_width))
146 self._append_plain_text(' ' * (spaces % self.tab_width))
145 147
146 148 def _prompt_finished_hook(self):
147 149 """ Called immediately after a prompt is finished, i.e. when some input
148 150 will be processed and a new prompt displayed.
149 151 """
150 152 if not self._reading:
151 153 self._highlighter.highlighting_on = False
152 154
153 155 def _tab_pressed(self):
154 156 """ Called when the tab key is pressed. Returns whether to continue
155 157 processing the event.
156 158 """
157 159 self._keep_cursor_in_buffer()
158 cursor = self.textCursor()
160 cursor = self._get_cursor()
159 161 return not self._complete()
160 162
161 163 #---------------------------------------------------------------------------
162 164 # 'FrontendWidget' interface
163 165 #---------------------------------------------------------------------------
164 166
165 167 def execute_file(self, path, hidden=False):
166 168 """ Attempts to execute file with 'path'. If 'hidden', no output is
167 169 shown.
168 170 """
169 171 self.execute('execfile("%s")' % path, hidden=hidden)
170 172
171 173 def _get_kernel_manager(self):
172 174 """ Returns the current kernel manager.
173 175 """
174 176 return self._kernel_manager
175 177
176 178 def _set_kernel_manager(self, kernel_manager):
177 179 """ Disconnect from the current kernel manager (if any) and set a new
178 180 kernel manager.
179 181 """
180 182 # Disconnect the old kernel manager, if necessary.
181 183 if self._kernel_manager is not None:
182 184 self._kernel_manager.started_channels.disconnect(
183 185 self._started_channels)
184 186 self._kernel_manager.stopped_channels.disconnect(
185 187 self._stopped_channels)
186 188
187 189 # Disconnect the old kernel manager's channels.
188 190 sub = self._kernel_manager.sub_channel
189 191 xreq = self._kernel_manager.xreq_channel
190 192 rep = self._kernel_manager.rep_channel
191 193 sub.message_received.disconnect(self._handle_sub)
192 194 xreq.execute_reply.disconnect(self._handle_execute_reply)
193 195 xreq.complete_reply.disconnect(self._handle_complete_reply)
194 196 xreq.object_info_reply.disconnect(self._handle_object_info_reply)
195 197 rep.input_requested.disconnect(self._handle_req)
196 198
197 199 # Handle the case where the old kernel manager is still listening.
198 200 if self._kernel_manager.channels_running:
199 201 self._stopped_channels()
200 202
201 203 # Set the new kernel manager.
202 204 self._kernel_manager = kernel_manager
203 205 if kernel_manager is None:
204 206 return
205 207
206 208 # Connect the new kernel manager.
207 209 kernel_manager.started_channels.connect(self._started_channels)
208 210 kernel_manager.stopped_channels.connect(self._stopped_channels)
209 211
210 212 # Connect the new kernel manager's channels.
211 213 sub = kernel_manager.sub_channel
212 214 xreq = kernel_manager.xreq_channel
213 215 rep = kernel_manager.rep_channel
214 216 sub.message_received.connect(self._handle_sub)
215 217 xreq.execute_reply.connect(self._handle_execute_reply)
216 218 xreq.complete_reply.connect(self._handle_complete_reply)
217 219 xreq.object_info_reply.connect(self._handle_object_info_reply)
218 220 rep.input_requested.connect(self._handle_req)
219 221
220 222 # Handle the case where the kernel manager started channels before
221 223 # we connected.
222 224 if kernel_manager.channels_running:
223 225 self._started_channels()
224 226
225 227 kernel_manager = property(_get_kernel_manager, _set_kernel_manager)
226 228
227 229 #---------------------------------------------------------------------------
228 230 # 'FrontendWidget' protected interface
229 231 #---------------------------------------------------------------------------
230 232
231 233 def _call_tip(self):
232 234 """ Shows a call tip, if appropriate, at the current cursor location.
233 235 """
234 236 # Decide if it makes sense to show a call tip
235 cursor = self.textCursor()
237 cursor = self._get_cursor()
236 238 cursor.movePosition(QtGui.QTextCursor.Left)
237 document = self.document()
239 document = self._control.document()
238 240 if document.characterAt(cursor.position()).toAscii() != '(':
239 241 return False
240 242 context = self._get_context(cursor)
241 243 if not context:
242 244 return False
243 245
244 246 # Send the metadata request to the kernel
245 247 name = '.'.join(context)
246 248 self._calltip_id = self.kernel_manager.xreq_channel.object_info(name)
247 self._calltip_pos = self.textCursor().position()
249 self._calltip_pos = self._get_cursor().position()
248 250 return True
249 251
250 252 def _complete(self):
251 253 """ Performs completion at the current cursor location.
252 254 """
253 255 # Decide if it makes sense to do completion
254 256 context = self._get_context()
255 257 if not context:
256 258 return False
257 259
258 260 # Send the completion request to the kernel
259 261 text = '.'.join(context)
260 262 self._complete_id = self.kernel_manager.xreq_channel.complete(
261 263 text, self.input_buffer_cursor_line, self.input_buffer)
262 self._complete_pos = self.textCursor().position()
264 self._complete_pos = self._get_cursor().position()
263 265 return True
264 266
265 267 def _get_banner(self):
266 268 """ Gets a banner to display at the beginning of a session.
267 269 """
268 270 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
269 271 '"license" for more information.'
270 272 return banner % (sys.version, sys.platform)
271 273
272 274 def _get_context(self, cursor=None):
273 275 """ Gets the context at the current cursor location.
274 276 """
275 277 if cursor is None:
276 cursor = self.textCursor()
278 cursor = self._get_cursor()
277 279 cursor.movePosition(QtGui.QTextCursor.StartOfLine,
278 280 QtGui.QTextCursor.KeepAnchor)
279 281 text = str(cursor.selection().toPlainText())
280 282 return self._completion_lexer.get_context(text)
281 283
282 284 def _interrupt_kernel(self):
283 285 """ Attempts to the interrupt the kernel.
284 286 """
285 287 if self.kernel_manager.has_kernel:
286 288 self.kernel_manager.signal_kernel(signal.SIGINT)
287 289 else:
288 self.appendPlainText('Kernel process is either remote or '
289 'unspecified. Cannot interrupt.\n')
290 self._append_plain_text('Kernel process is either remote or '
291 'unspecified. Cannot interrupt.\n')
290 292
291 293 def _show_interpreter_prompt(self):
292 294 """ Shows a prompt for the interpreter.
293 295 """
294 296 self._show_prompt('>>> ')
295 297
296 298 #------ Signal handlers ----------------------------------------------------
297 299
298 300 def _started_channels(self):
299 301 """ Called when the kernel manager has started listening.
300 302 """
301 303 self._reset()
302 self.appendPlainText(self._get_banner())
304 self._append_plain_text(self._get_banner())
303 305 self._show_interpreter_prompt()
304 306
305 307 def _stopped_channels(self):
306 308 """ Called when the kernel manager has stopped listening.
307 309 """
308 310 # FIXME: Print a message here?
309 311 pass
310 312
311 313 def _document_contents_change(self, position, removed, added):
312 314 """ Called whenever the document's content changes. Display a calltip
313 315 if appropriate.
314 316 """
315 317 # Calculate where the cursor should be *after* the change:
316 318 position += added
317 319
318 document = self.document()
319 if position == self.textCursor().position():
320 document = self._control.document()
321 if position == self._get_cursor().position():
320 322 self._call_tip()
321 323
322 324 def _handle_req(self, req):
323 325 # Make sure that all output from the SUB channel has been processed
324 326 # before entering readline mode.
325 327 self.kernel_manager.sub_channel.flush()
326 328
327 329 def callback(line):
328 330 self.kernel_manager.rep_channel.input(line)
329 331 self._readline(req['content']['prompt'], callback=callback)
330 332
331 333 def _handle_sub(self, omsg):
332 334 if self._hidden:
333 335 return
334 336 handler = getattr(self, '_handle_%s' % omsg['msg_type'], None)
335 337 if handler is not None:
336 338 handler(omsg)
337 339
338 340 def _handle_pyout(self, omsg):
339 self.appendPlainText(omsg['content']['data'] + '\n')
341 self._append_plain_text(omsg['content']['data'] + '\n')
340 342
341 343 def _handle_stream(self, omsg):
342 self.appendPlainText(omsg['content']['data'])
343 self.moveCursor(QtGui.QTextCursor.End)
344 self._append_plain_text(omsg['content']['data'])
345 self._control.moveCursor(QtGui.QTextCursor.End)
344 346
345 347 def _handle_execute_reply(self, reply):
346 348 if self._hidden:
347 349 return
348 350
349 351 # Make sure that all output from the SUB channel has been processed
350 352 # before writing a new prompt.
351 353 self.kernel_manager.sub_channel.flush()
352 354
353 355 status = reply['content']['status']
354 356 if status == 'error':
355 357 self._handle_execute_error(reply)
356 358 elif status == 'aborted':
357 359 text = "ERROR: ABORTED\n"
358 self.appendPlainText(text)
360 self._append_plain_text(text)
359 361 self._hidden = True
360 362 self._show_interpreter_prompt()
361 363 self.executed.emit(reply)
362 364
363 365 def _handle_execute_error(self, reply):
364 366 content = reply['content']
365 367 traceback = ''.join(content['traceback'])
366 self.appendPlainText(traceback)
368 self._append_plain_text(traceback)
367 369
368 370 def _handle_complete_reply(self, rep):
369 cursor = self.textCursor()
371 cursor = self._get_cursor()
370 372 if rep['parent_header']['msg_id'] == self._complete_id and \
371 373 cursor.position() == self._complete_pos:
372 374 text = '.'.join(self._get_context())
373 375 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
374 376 self._complete_with_items(cursor, rep['content']['matches'])
375 377
376 378 def _handle_object_info_reply(self, rep):
377 cursor = self.textCursor()
379 cursor = self._get_cursor()
378 380 if rep['parent_header']['msg_id'] == self._calltip_id and \
379 381 cursor.position() == self._calltip_pos:
380 382 doc = rep['content']['docstring']
381 383 if doc:
382 384 self._call_tip_widget.show_docstring(doc)
@@ -1,206 +1,206
1 1 # System library imports
2 2 from PyQt4 import QtCore, QtGui
3 3
4 4 # Local imports
5 5 from IPython.core.usage import default_banner
6 6 from frontend_widget import FrontendWidget
7 7
8 8
9 9 class IPythonWidget(FrontendWidget):
10 10 """ A FrontendWidget for an IPython kernel.
11 11 """
12 12
13 13 # The default stylesheet: black text on a white background.
14 14 default_stylesheet = """
15 15 .error { color: red; }
16 16 .in-prompt { color: navy; }
17 17 .in-prompt-number { font-weight: bold; }
18 18 .out-prompt { color: darkred; }
19 19 .out-prompt-number { font-weight: bold; }
20 20 """
21 21
22 22 # A dark stylesheet: white text on a black background.
23 23 dark_stylesheet = """
24 24 QPlainTextEdit { background-color: black; color: white }
25 25 QFrame { border: 1px solid grey; }
26 26 .error { color: red; }
27 27 .in-prompt { color: lime; }
28 28 .in-prompt-number { color: lime; font-weight: bold; }
29 29 .out-prompt { color: red; }
30 30 .out-prompt-number { color: red; font-weight: bold; }
31 31 """
32 32
33 33 # Default prompts.
34 34 in_prompt = '<br/>In [<span class="in-prompt-number">%i</span>]: '
35 35 out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
36 36
37 37 #---------------------------------------------------------------------------
38 # 'QObject' interface
38 # 'object' interface
39 39 #---------------------------------------------------------------------------
40 40
41 def __init__(self, parent=None):
42 super(IPythonWidget, self).__init__(parent)
41 def __init__(self, *args, **kw):
42 super(IPythonWidget, self).__init__(*args, **kw)
43 43
44 44 # Initialize protected variables.
45 45 self._previous_prompt_blocks = []
46 46 self._prompt_count = 0
47 47
48 48 # Set a default stylesheet.
49 49 self.reset_styling()
50 50
51 51 #---------------------------------------------------------------------------
52 52 # 'FrontendWidget' interface
53 53 #---------------------------------------------------------------------------
54 54
55 55 def execute_file(self, path, hidden=False):
56 56 """ Reimplemented to use the 'run' magic.
57 57 """
58 58 self.execute('run %s' % path, hidden=hidden)
59 59
60 60 #---------------------------------------------------------------------------
61 61 # 'FrontendWidget' protected interface
62 62 #---------------------------------------------------------------------------
63 63
64 64 def _get_banner(self):
65 65 """ Reimplemented to return IPython's default banner.
66 66 """
67 67 return default_banner
68 68
69 69 def _show_interpreter_prompt(self):
70 70 """ Reimplemented for IPython-style prompts.
71 71 """
72 72 # Update old prompt numbers if necessary.
73 73 previous_prompt_number = self._prompt_count
74 74 if previous_prompt_number != self._prompt_count:
75 75 for i, (block, length) in enumerate(self._previous_prompt_blocks):
76 76 if block.isValid():
77 77 cursor = QtGui.QTextCursor(block)
78 78 cursor.movePosition(QtGui.QTextCursor.Right,
79 79 QtGui.QTextCursor.KeepAnchor, length-1)
80 80 if i == 0:
81 81 prompt = self._make_in_prompt(previous_prompt_number)
82 82 else:
83 83 prompt = self._make_out_prompt(previous_prompt_number)
84 84 self._insert_html(cursor, prompt)
85 85 self._previous_prompt_blocks = []
86 86
87 87 # Show a new prompt.
88 88 self._prompt_count += 1
89 89 self._show_prompt(self._make_in_prompt(self._prompt_count), html=True)
90 90 self._save_prompt_block()
91 91
92 92 # Update continuation prompt to reflect (possibly) new prompt length.
93 93 self._set_continuation_prompt(
94 94 self._make_continuation_prompt(self._prompt), html=True)
95 95
96 96 #------ Signal handlers ----------------------------------------------------
97 97
98 98 def _handle_execute_error(self, reply):
99 99 """ Reimplemented for IPython-style traceback formatting.
100 100 """
101 101 content = reply['content']
102 102 traceback_lines = content['traceback'][:]
103 103 traceback = ''.join(traceback_lines)
104 104 traceback = traceback.replace(' ', '&nbsp;')
105 105 traceback = traceback.replace('\n', '<br/>')
106 106
107 107 ename = content['ename']
108 108 ename_styled = '<span class="error">%s</span>' % ename
109 109 traceback = traceback.replace(ename, ename_styled)
110 110
111 self.appendHtml(traceback)
111 self._append_html(traceback)
112 112
113 113 def _handle_pyout(self, omsg):
114 114 """ Reimplemented for IPython-style "display hook".
115 115 """
116 self.appendHtml(self._make_out_prompt(self._prompt_count))
116 self._append_html(self._make_out_prompt(self._prompt_count))
117 117 self._save_prompt_block()
118 118
119 self.appendPlainText(omsg['content']['data'] + '\n')
119 self._append_plain_text(omsg['content']['data'] + '\n')
120 120
121 121 #---------------------------------------------------------------------------
122 122 # 'IPythonWidget' interface
123 123 #---------------------------------------------------------------------------
124 124
125 125 def reset_styling(self):
126 126 """ Restores the default IPythonWidget styling.
127 127 """
128 128 self.set_styling(self.default_stylesheet, syntax_style='default')
129 129 #self.set_styling(self.dark_stylesheet, syntax_style='monokai')
130 130
131 131 def set_styling(self, stylesheet, syntax_style=None):
132 132 """ Sets the IPythonWidget styling.
133 133
134 134 Parameters:
135 135 -----------
136 136 stylesheet : str
137 137 A CSS stylesheet. The stylesheet can contain classes for:
138 138 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
139 139 2. Pygments: .c, .k, .o, etc (see PygmentsHighlighter)
140 140 3. IPython: .error, .in-prompt, .out-prompt, etc.
141 141
142 142 syntax_style : str or None [default None]
143 143 If specified, use the Pygments style with given name. Otherwise,
144 144 the stylesheet is queried for Pygments style information.
145 145 """
146 146 self.setStyleSheet(stylesheet)
147 self.document().setDefaultStyleSheet(stylesheet)
147 self._control.document().setDefaultStyleSheet(stylesheet)
148 148
149 149 if syntax_style is None:
150 150 self._highlighter.set_style_sheet(stylesheet)
151 151 else:
152 152 self._highlighter.set_style(syntax_style)
153 153
154 154 #---------------------------------------------------------------------------
155 155 # 'IPythonWidget' protected interface
156 156 #---------------------------------------------------------------------------
157 157
158 158 def _make_in_prompt(self, number):
159 159 """ Given a prompt number, returns an HTML In prompt.
160 160 """
161 161 body = self.in_prompt % number
162 162 return '<span class="in-prompt">%s</span>' % body
163 163
164 164 def _make_continuation_prompt(self, prompt):
165 165 """ Given a plain text version of an In prompt, returns an HTML
166 166 continuation prompt.
167 167 """
168 168 end_chars = '...: '
169 169 space_count = len(prompt.lstrip('\n')) - len(end_chars)
170 170 body = '&nbsp;' * space_count + end_chars
171 171 return '<span class="in-prompt">%s</span>' % body
172 172
173 173 def _make_out_prompt(self, number):
174 174 """ Given a prompt number, returns an HTML Out prompt.
175 175 """
176 176 body = self.out_prompt % number
177 177 return '<span class="out-prompt">%s</span>' % body
178 178
179 179 def _save_prompt_block(self):
180 180 """ Assuming a prompt has just been written at the end of the buffer,
181 181 store the QTextBlock that contains it and its length.
182 182 """
183 block = self.document().lastBlock()
183 block = self._control.document().lastBlock()
184 184 self._previous_prompt_blocks.append((block, block.length()))
185 185
186 186
187 187 if __name__ == '__main__':
188 188 from IPython.frontend.qt.kernelmanager import QtKernelManager
189 189
190 190 # Don't let Qt or ZMQ swallow KeyboardInterupts.
191 191 import signal
192 192 signal.signal(signal.SIGINT, signal.SIG_DFL)
193 193
194 194 # Create a KernelManager.
195 195 kernel_manager = QtKernelManager()
196 196 kernel_manager.start_kernel()
197 197 kernel_manager.start_channels()
198 198
199 199 # Launch the application.
200 200 app = QtGui.QApplication([])
201 201 widget = IPythonWidget()
202 202 widget.kernel_manager = kernel_manager
203 203 widget.setWindowTitle('Python')
204 204 widget.resize(640, 480)
205 205 widget.show()
206 206 app.exec_()
General Comments 0
You need to be logged in to leave comments. Login now