##// END OF EJS Templates
* Added 'started_listening' and 'stopped_listening' signals to QtKernelManager. The FrontendWidget listens for these signals....
epatters -
Show More
@@ -0,0 +1,25 b''
1 """ Defines miscellaneous Qt-related helper classes and functions.
2 """
3
4 # System library imports.
5 from PyQt4 import QtCore
6
7 # IPython imports.
8 from IPython.utils.traitlets import HasTraits
9
10
11 MetaHasTraits = type(HasTraits)
12 MetaQObject = type(QtCore.QObject)
13
14 class MetaQObjectHasTraits(MetaQObject, MetaHasTraits):
15 """ A metaclass that inherits from the metaclasses of both HasTraits and
16 QObject.
17
18 Using this metaclass allows a class to inherit from both HasTraits and
19 QObject. See QtKernelManager for an example.
20 """
21
22 def __init__(cls, name, bases, dct):
23 MetaQObject.__init__(cls, name, bases, dct)
24 MetaHasTraits.__init__(cls, name, bases, dct)
25
@@ -1,674 +1,688 b''
1 1 # Standard library imports
2 2 import re
3 3
4 4 # System library imports
5 5 from PyQt4 import QtCore, QtGui
6 6
7 7 # Local imports
8 8 from completion_widget import CompletionWidget
9 9
10 10
11 11 class AnsiCodeProcessor(object):
12 12 """ Translates ANSI escape codes into readable attributes.
13 13 """
14 14
15 15 def __init__(self):
16 16 self.ansi_colors = ( # Normal, Bright/Light
17 17 ('#000000', '#7f7f7f'), # 0: black
18 18 ('#cd0000', '#ff0000'), # 1: red
19 19 ('#00cd00', '#00ff00'), # 2: green
20 20 ('#cdcd00', '#ffff00'), # 3: yellow
21 21 ('#0000ee', '#0000ff'), # 4: blue
22 22 ('#cd00cd', '#ff00ff'), # 5: magenta
23 23 ('#00cdcd', '#00ffff'), # 6: cyan
24 24 ('#e5e5e5', '#ffffff')) # 7: white
25 25 self.reset()
26 26
27 27 def set_code(self, code):
28 28 """ Set attributes based on code.
29 29 """
30 30 if code == 0:
31 31 self.reset()
32 32 elif code == 1:
33 33 self.intensity = 1
34 34 self.bold = True
35 35 elif code == 3:
36 36 self.italic = True
37 37 elif code == 4:
38 38 self.underline = True
39 39 elif code == 22:
40 40 self.intensity = 0
41 41 self.bold = False
42 42 elif code == 23:
43 43 self.italic = False
44 44 elif code == 24:
45 45 self.underline = False
46 46 elif code >= 30 and code <= 37:
47 47 self.foreground_color = code - 30
48 48 elif code == 39:
49 49 self.foreground_color = None
50 50 elif code >= 40 and code <= 47:
51 51 self.background_color = code - 40
52 52 elif code == 49:
53 53 self.background_color = None
54 54
55 55 def reset(self):
56 56 """ Reset attributs to their default values.
57 57 """
58 58 self.intensity = 0
59 59 self.italic = False
60 60 self.bold = False
61 61 self.underline = False
62 62 self.foreground_color = None
63 63 self.background_color = None
64 64
65 65
66 66 class QtAnsiCodeProcessor(AnsiCodeProcessor):
67 67 """ Translates ANSI escape codes into QTextCharFormats.
68 68 """
69 69
70 70 def get_format(self):
71 71 """ Returns a QTextCharFormat that encodes the current style attributes.
72 72 """
73 73 format = QtGui.QTextCharFormat()
74 74
75 75 # Set foreground color
76 76 if self.foreground_color is not None:
77 77 color = self.ansi_colors[self.foreground_color][self.intensity]
78 78 format.setForeground(QtGui.QColor(color))
79 79
80 80 # Set background color
81 81 if self.background_color is not None:
82 82 color = self.ansi_colors[self.background_color][self.intensity]
83 83 format.setBackground(QtGui.QColor(color))
84 84
85 85 # Set font weight/style options
86 86 if self.bold:
87 87 format.setFontWeight(QtGui.QFont.Bold)
88 88 else:
89 89 format.setFontWeight(QtGui.QFont.Normal)
90 90 format.setFontItalic(self.italic)
91 91 format.setFontUnderline(self.underline)
92 92
93 93 return format
94 94
95 95
96 96 class ConsoleWidget(QtGui.QPlainTextEdit):
97 97 """ Base class for console-type widgets. This class is mainly concerned with
98 98 dealing with the prompt, keeping the cursor inside the editing line, and
99 99 handling ANSI escape sequences.
100 100 """
101 101
102 102 # Regex to match ANSI escape sequences
103 103 _ansi_pattern = re.compile('\x01?\x1b\[(.*?)m\x02?')
104 104
105 105 # When ctrl is pressed, map certain keys to other keys (without the ctrl):
106 106 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
107 107 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
108 108 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
109 109 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
110 110 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
111 111 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
112 112 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
113 113
114 114 #---------------------------------------------------------------------------
115 115 # 'QWidget' interface
116 116 #---------------------------------------------------------------------------
117 117
118 118 def __init__(self, parent=None):
119 119 QtGui.QPlainTextEdit.__init__(self, parent)
120 120
121 121 # Initialize public and protected variables
122 122 self.ansi_codes = True
123 123 self.buffer_size = 500
124 self.continuation_prompt = '> '
125 124 self.gui_completion = True
126 125 self._ansi_processor = QtAnsiCodeProcessor()
127 126 self._completion_widget = CompletionWidget(self)
127 self._continuation_prompt = '> '
128 128 self._executing = False
129 129 self._prompt = ''
130 130 self._prompt_pos = 0
131 131 self._reading = False
132 132
133 133 # Set a monospaced font
134 134 point_size = QtGui.QApplication.font().pointSize()
135 135 font = QtGui.QFont('Monospace', point_size)
136 136 font.setStyleHint(QtGui.QFont.TypeWriter)
137 137 self._completion_widget.setFont(font)
138 138 self.document().setDefaultFont(font)
139 139
140 140 # Define a custom context menu
141 141 self._context_menu = QtGui.QMenu(self)
142 142
143 143 copy_action = QtGui.QAction('Copy', self)
144 144 copy_action.triggered.connect(self.copy)
145 145 self.copyAvailable.connect(copy_action.setEnabled)
146 146 self._context_menu.addAction(copy_action)
147 147
148 148 self._paste_action = QtGui.QAction('Paste', self)
149 149 self._paste_action.triggered.connect(self.paste)
150 150 self._context_menu.addAction(self._paste_action)
151 151 self._context_menu.addSeparator()
152 152
153 153 select_all_action = QtGui.QAction('Select All', self)
154 154 select_all_action.triggered.connect(self.selectAll)
155 155 self._context_menu.addAction(select_all_action)
156 156
157 157 def contextMenuEvent(self, event):
158 158 """ Reimplemented to create a menu without destructive actions like
159 159 'Cut' and 'Delete'.
160 160 """
161 161 clipboard_empty = QtGui.QApplication.clipboard().text().isEmpty()
162 162 self._paste_action.setEnabled(not clipboard_empty)
163 163
164 164 self._context_menu.exec_(event.globalPos())
165 165
166 166 def keyPressEvent(self, event):
167 167 """ Reimplemented to create a console-like interface.
168 168 """
169 169 intercepted = False
170 170 cursor = self.textCursor()
171 171 position = cursor.position()
172 172 key = event.key()
173 173 ctrl_down = event.modifiers() & QtCore.Qt.ControlModifier
174 174 alt_down = event.modifiers() & QtCore.Qt.AltModifier
175 175 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
176 176
177 177 # Even though we have reimplemented 'paste', the C++ level slot is still
178 178 # called by Qt. So we intercept the key press here.
179 179 if event.matches(QtGui.QKeySequence.Paste):
180 180 self.paste()
181 181 intercepted = True
182 182
183 183 elif ctrl_down:
184 184 if key in self._ctrl_down_remap:
185 185 ctrl_down = False
186 186 key = self._ctrl_down_remap[key]
187 187 event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, key,
188 188 QtCore.Qt.NoModifier)
189 189
190 190 elif key == QtCore.Qt.Key_K:
191 191 if self._in_buffer(position):
192 192 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
193 193 QtGui.QTextCursor.KeepAnchor)
194 194 cursor.removeSelectedText()
195 195 intercepted = True
196 196
197 197 elif key == QtCore.Qt.Key_Y:
198 198 self.paste()
199 199 intercepted = True
200 200
201 201 elif alt_down:
202 202 if key == QtCore.Qt.Key_B:
203 203 self.setTextCursor(self._get_word_start_cursor(position))
204 204 intercepted = True
205 205
206 206 elif key == QtCore.Qt.Key_F:
207 207 self.setTextCursor(self._get_word_end_cursor(position))
208 208 intercepted = True
209 209
210 210 elif key == QtCore.Qt.Key_Backspace:
211 211 cursor = self._get_word_start_cursor(position)
212 212 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
213 213 cursor.removeSelectedText()
214 214 intercepted = True
215 215
216 216 elif key == QtCore.Qt.Key_D:
217 217 cursor = self._get_word_end_cursor(position)
218 218 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
219 219 cursor.removeSelectedText()
220 220 intercepted = True
221 221
222 222 if self._completion_widget.isVisible():
223 223 self._completion_widget.keyPressEvent(event)
224 224 intercepted = event.isAccepted()
225 225
226 226 else:
227 227 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
228 228 if self._reading:
229 229 self._reading = False
230 230 elif not self._executing:
231 231 self.execute(interactive=True)
232 232 intercepted = True
233 233
234 234 elif key == QtCore.Qt.Key_Up:
235 235 if self._reading or not self._up_pressed():
236 236 intercepted = True
237 237 else:
238 238 prompt_line = self._get_prompt_cursor().blockNumber()
239 239 intercepted = cursor.blockNumber() <= prompt_line
240 240
241 241 elif key == QtCore.Qt.Key_Down:
242 242 if self._reading or not self._down_pressed():
243 243 intercepted = True
244 244 else:
245 245 end_line = self._get_end_cursor().blockNumber()
246 246 intercepted = cursor.blockNumber() == end_line
247 247
248 248 elif key == QtCore.Qt.Key_Tab:
249 249 if self._reading:
250 250 intercepted = False
251 251 else:
252 252 intercepted = not self._tab_pressed()
253 253
254 254 elif key == QtCore.Qt.Key_Left:
255 255 intercepted = not self._in_buffer(position - 1)
256 256
257 257 elif key == QtCore.Qt.Key_Home:
258 258 cursor.movePosition(QtGui.QTextCursor.StartOfLine)
259 259 start_pos = cursor.position()
260 260 start_line = cursor.blockNumber()
261 261 if start_line == self._get_prompt_cursor().blockNumber():
262 262 start_pos += len(self._prompt)
263 263 else:
264 start_pos += len(self.continuation_prompt)
264 start_pos += len(self._continuation_prompt)
265 265 if shift_down and self._in_buffer(position):
266 266 self._set_selection(position, start_pos)
267 267 else:
268 268 self._set_position(start_pos)
269 269 intercepted = True
270 270
271 271 elif key == QtCore.Qt.Key_Backspace and not alt_down:
272 272
273 273 # Line deletion (remove continuation prompt)
274 len_prompt = len(self.continuation_prompt)
274 len_prompt = len(self._continuation_prompt)
275 275 if cursor.columnNumber() == len_prompt and \
276 276 position != self._prompt_pos:
277 277 cursor.setPosition(position - len_prompt,
278 278 QtGui.QTextCursor.KeepAnchor)
279 279 cursor.removeSelectedText()
280 280
281 281 # Regular backwards deletion
282 282 else:
283 283 anchor = cursor.anchor()
284 284 if anchor == position:
285 285 intercepted = not self._in_buffer(position - 1)
286 286 else:
287 287 intercepted = not self._in_buffer(min(anchor, position))
288 288
289 289 elif key == QtCore.Qt.Key_Delete:
290 290 anchor = cursor.anchor()
291 291 intercepted = not self._in_buffer(min(anchor, position))
292 292
293 293 # Don't move cursor if control is down to allow copy-paste using
294 294 # the keyboard in any part of the buffer.
295 295 if not ctrl_down:
296 296 self._keep_cursor_in_buffer()
297 297
298 298 if not intercepted:
299 299 QtGui.QPlainTextEdit.keyPressEvent(self, event)
300 300
301 301 #--------------------------------------------------------------------------
302 302 # 'QPlainTextEdit' interface
303 303 #--------------------------------------------------------------------------
304 304
305 305 def appendPlainText(self, text):
306 306 """ Reimplemented to not append text as a new paragraph, which doesn't
307 307 make sense for a console widget. Also, if enabled, handle ANSI
308 308 codes.
309 309 """
310 310 cursor = self.textCursor()
311 311 cursor.movePosition(QtGui.QTextCursor.End)
312 312
313 313 if self.ansi_codes:
314 314 format = QtGui.QTextCharFormat()
315 315 previous_end = 0
316 316 for match in self._ansi_pattern.finditer(text):
317 317 cursor.insertText(text[previous_end:match.start()], format)
318 318 previous_end = match.end()
319 319 for code in match.group(1).split(';'):
320 320 self._ansi_processor.set_code(int(code))
321 321 format = self._ansi_processor.get_format()
322 322 cursor.insertText(text[previous_end:], format)
323 323 else:
324 324 cursor.insertText(text)
325 325
326 def clear(self, keep_input=False):
327 """ Reimplemented to write a new prompt. If 'keep_input' is set,
328 restores the old input buffer when the new prompt is written.
329 """
330 super(ConsoleWidget, self).clear()
331
332 if keep_input:
333 input_buffer = self.input_buffer
334 self._show_prompt()
335 if keep_input:
336 self.input_buffer = input_buffer
337
326 338 def paste(self):
327 339 """ Reimplemented to ensure that text is pasted in the editing region.
328 340 """
329 341 self._keep_cursor_in_buffer()
330 342 QtGui.QPlainTextEdit.paste(self)
331 343
332 344 def print_(self, printer):
333 """ Reimplemented to work around bug in PyQt where the C++ level
334 'print_' slot has the wrong signature.
345 """ Reimplemented to work around a bug in PyQt: the C++ level 'print_'
346 slot has the wrong signature.
335 347 """
336 348 QtGui.QPlainTextEdit.print_(self, printer)
337 349
338 350 #---------------------------------------------------------------------------
339 351 # 'ConsoleWidget' public interface
340 352 #---------------------------------------------------------------------------
341 353
342 354 def execute(self, interactive=False):
343 355 """ Execute the text in the input buffer. Returns whether the input
344 356 buffer was completely processed and a new prompt created.
345 357 """
346 358 self.appendPlainText('\n')
347 359 self._executing_input_buffer = self.input_buffer
348 360 self._executing = True
349 361 self._prompt_finished()
350 362 return self._execute(interactive=interactive)
351 363
352 364 def _get_input_buffer(self):
353 365 # If we're executing, the input buffer may not even exist anymore due
354 366 # the limit imposed by 'buffer_size'. Therefore, we store it.
355 367 if self._executing:
356 368 return self._executing_input_buffer
357 369
358 370 cursor = self._get_end_cursor()
359 371 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
360 372
361 373 # Use QTextDocumentFragment intermediate object because it strips
362 374 # out the Unicode line break characters that Qt insists on inserting.
363 375 input_buffer = str(cursor.selection().toPlainText())
364 376
365 377 # Strip out continuation prompts
366 return input_buffer.replace('\n' + self.continuation_prompt, '\n')
378 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
367 379
368 380 def _set_input_buffer(self, string):
369 381 # Add continuation prompts where necessary
370 382 lines = string.splitlines()
371 383 for i in xrange(1, len(lines)):
372 lines[i] = self.continuation_prompt + lines[i]
384 lines[i] = self._continuation_prompt + lines[i]
373 385 string = '\n'.join(lines)
374 386
375 387 # Replace buffer with new text
376 388 cursor = self._get_end_cursor()
377 389 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
378 390 cursor.insertText(string)
379 391 self.moveCursor(QtGui.QTextCursor.End)
380 392
381 393 input_buffer = property(_get_input_buffer, _set_input_buffer)
382 394
383 395 def _get_input_buffer_cursor_line(self):
384 396 if self._executing:
385 397 return None
386 398 cursor = self.textCursor()
387 399 if cursor.position() >= self._prompt_pos:
388 400 text = str(cursor.block().text())
389 401 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
390 402 return text[len(self._prompt):]
391 403 else:
392 return text[len(self.continuation_prompt):]
404 return text[len(self._continuation_prompt):]
393 405 else:
394 406 return None
395 407
396 408 input_buffer_cursor_line = property(_get_input_buffer_cursor_line)
397 409
398 410 #---------------------------------------------------------------------------
399 411 # 'ConsoleWidget' abstract interface
400 412 #---------------------------------------------------------------------------
401 413
402 414 def _execute(self, interactive):
403 415 """ Called to execute the input buffer. When triggered by an the enter
404 416 key press, 'interactive' is True; otherwise, it is False. Returns
405 417 whether the input buffer was completely processed and a new prompt
406 418 created.
407 419 """
408 420 raise NotImplementedError
409 421
410 422 def _prompt_started_hook(self):
411 423 """ Called immediately after a new prompt is displayed.
412 424 """
413 425 pass
414 426
415 427 def _prompt_finished_hook(self):
416 428 """ Called immediately after a prompt is finished, i.e. when some input
417 429 will be processed and a new prompt displayed.
418 430 """
419 431 pass
420 432
421 433 def _up_pressed(self):
422 434 """ Called when the up key is pressed. Returns whether to continue
423 435 processing the event.
424 436 """
425 437 return True
426 438
427 439 def _down_pressed(self):
428 440 """ Called when the down key is pressed. Returns whether to continue
429 441 processing the event.
430 442 """
431 443 return True
432 444
433 445 def _tab_pressed(self):
434 446 """ Called when the tab key is pressed. Returns whether to continue
435 447 processing the event.
436 448 """
437 449 return False
438 450
439 451 #--------------------------------------------------------------------------
440 452 # 'ConsoleWidget' protected interface
441 453 #--------------------------------------------------------------------------
442 454
443 455 def _complete_with_items(self, cursor, items):
444 456 """ Performs completion with 'items' at the specified cursor location.
445 457 """
446 458 if len(items) == 1:
447 459 cursor.setPosition(self.textCursor().position(),
448 460 QtGui.QTextCursor.KeepAnchor)
449 461 cursor.insertText(items[0])
450 462 elif len(items) > 1:
451 463 if self.gui_completion:
452 464 self._completion_widget.show_items(cursor, items)
453 465 else:
454 466 text = '\n'.join(items) + '\n'
455 467 self._write_text_keeping_prompt(text)
456 468
457 469 def _get_end_cursor(self):
458 470 """ Convenience method that returns a cursor for the last character.
459 471 """
460 472 cursor = self.textCursor()
461 473 cursor.movePosition(QtGui.QTextCursor.End)
462 474 return cursor
463 475
464 476 def _get_prompt_cursor(self):
465 477 """ Convenience method that returns a cursor for the prompt position.
466 478 """
467 479 cursor = self.textCursor()
468 480 cursor.setPosition(self._prompt_pos)
469 481 return cursor
470 482
471 483 def _get_selection_cursor(self, start, end):
472 484 """ Convenience method that returns a cursor with text selected between
473 485 the positions 'start' and 'end'.
474 486 """
475 487 cursor = self.textCursor()
476 488 cursor.setPosition(start)
477 489 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
478 490 return cursor
479 491
480 492 def _get_word_start_cursor(self, position):
481 493 """ Find the start of the word to the left the given position. If a
482 494 sequence of non-word characters precedes the first word, skip over
483 495 them. (This emulates the behavior of bash, emacs, etc.)
484 496 """
485 497 document = self.document()
486 498 position -= 1
487 499 while self._in_buffer(position) and \
488 500 not document.characterAt(position).isLetterOrNumber():
489 501 position -= 1
490 502 while self._in_buffer(position) and \
491 503 document.characterAt(position).isLetterOrNumber():
492 504 position -= 1
493 505 cursor = self.textCursor()
494 506 cursor.setPosition(position + 1)
495 507 return cursor
496 508
497 509 def _get_word_end_cursor(self, position):
498 510 """ Find the end of the word to the right the given position. If a
499 511 sequence of non-word characters precedes the first word, skip over
500 512 them. (This emulates the behavior of bash, emacs, etc.)
501 513 """
502 514 document = self.document()
503 515 end = self._get_end_cursor().position()
504 516 while position < end and \
505 517 not document.characterAt(position).isLetterOrNumber():
506 518 position += 1
507 519 while position < end and \
508 520 document.characterAt(position).isLetterOrNumber():
509 521 position += 1
510 522 cursor = self.textCursor()
511 523 cursor.setPosition(position)
512 524 return cursor
513 525
514 526 def _prompt_started(self):
515 527 """ Called immediately after a new prompt is displayed.
516 528 """
517 529 # Temporarily disable the maximum block count to permit undo/redo.
518 530 self.setMaximumBlockCount(0)
519 531 self.setUndoRedoEnabled(True)
520 532
521 533 self.setReadOnly(False)
522 534 self.moveCursor(QtGui.QTextCursor.End)
523 535 self.centerCursor()
524 536
525 537 self._executing = False
526 538 self._prompt_started_hook()
527 539
528 540 def _prompt_finished(self):
529 541 """ Called immediately after a prompt is finished, i.e. when some input
530 542 will be processed and a new prompt displayed.
531 543 """
532 544 # This has the (desired) side effect of disabling the undo/redo history.
533 545 self.setMaximumBlockCount(self.buffer_size)
534 546
535 547 self.setReadOnly(True)
536 548 self._prompt_finished_hook()
537 549
538 550 def _set_position(self, position):
539 551 """ Convenience method to set the position of the cursor.
540 552 """
541 553 cursor = self.textCursor()
542 554 cursor.setPosition(position)
543 555 self.setTextCursor(cursor)
544 556
545 557 def _set_selection(self, start, end):
546 558 """ Convenience method to set the current selected text.
547 559 """
548 560 self.setTextCursor(self._get_selection_cursor(start, end))
549 561
550 def _show_prompt(self, prompt):
551 """ Writes a new prompt at the end of the buffer.
562 def _show_prompt(self, prompt=None):
563 """ Writes a new prompt at the end of the buffer. If 'prompt' is not
564 specified, uses the previous prompt.
552 565 """
553 self.appendPlainText('\n' + prompt)
554 self._prompt = prompt
566 if prompt is not None:
567 self._prompt = prompt
568 self.appendPlainText('\n' + self._prompt)
555 569 self._prompt_pos = self._get_end_cursor().position()
556 570 self._prompt_started()
557 571
558 572 def _show_continuation_prompt(self):
559 573 """ Writes a new continuation prompt at the end of the buffer.
560 574 """
561 self.appendPlainText(self.continuation_prompt)
575 self.appendPlainText(self._continuation_prompt)
562 576 self._prompt_started()
563 577
564 578 def _write_text_keeping_prompt(self, text):
565 579 """ Writes 'text' after the current prompt, then restores the old prompt
566 580 with its old input buffer.
567 581 """
568 582 input_buffer = self.input_buffer
569 583 self.appendPlainText('\n')
570 584 self._prompt_finished()
571 585
572 586 self.appendPlainText(text)
573 self._show_prompt(self._prompt)
587 self._show_prompt()
574 588 self.input_buffer = input_buffer
575 589
576 590 def _in_buffer(self, position):
577 591 """ Returns whether the given position is inside the editing region.
578 592 """
579 593 return position >= self._prompt_pos
580 594
581 595 def _keep_cursor_in_buffer(self):
582 596 """ Ensures that the cursor is inside the editing region. Returns
583 597 whether the cursor was moved.
584 598 """
585 599 cursor = self.textCursor()
586 600 if cursor.position() < self._prompt_pos:
587 601 cursor.movePosition(QtGui.QTextCursor.End)
588 602 self.setTextCursor(cursor)
589 603 return True
590 604 else:
591 605 return False
592 606
593 607
594 608 class HistoryConsoleWidget(ConsoleWidget):
595 609 """ A ConsoleWidget that keeps a history of the commands that have been
596 610 executed.
597 611 """
598 612
599 613 #---------------------------------------------------------------------------
600 614 # 'QWidget' interface
601 615 #---------------------------------------------------------------------------
602 616
603 617 def __init__(self, parent=None):
604 618 super(HistoryConsoleWidget, self).__init__(parent)
605 619
606 620 self._history = []
607 621 self._history_index = 0
608 622
609 623 #---------------------------------------------------------------------------
610 624 # 'ConsoleWidget' public interface
611 625 #---------------------------------------------------------------------------
612 626
613 627 def execute(self, interactive=False):
614 628 """ Reimplemented to the store history.
615 629 """
616 630 stripped = self.input_buffer.rstrip()
617 631 executed = super(HistoryConsoleWidget, self).execute(interactive)
618 632 if executed:
619 633 self._history.append(stripped)
620 634 self._history_index = len(self._history)
621 635 return executed
622 636
623 637 #---------------------------------------------------------------------------
624 638 # 'ConsoleWidget' abstract interface
625 639 #---------------------------------------------------------------------------
626 640
627 641 def _up_pressed(self):
628 642 """ Called when the up key is pressed. Returns whether to continue
629 643 processing the event.
630 644 """
631 645 prompt_cursor = self._get_prompt_cursor()
632 646 if self.textCursor().blockNumber() == prompt_cursor.blockNumber():
633 647 self.history_previous()
634 648
635 649 # Go to the first line of prompt for seemless history scrolling.
636 650 cursor = self._get_prompt_cursor()
637 651 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
638 652 self.setTextCursor(cursor)
639 653
640 654 return False
641 655 return True
642 656
643 657 def _down_pressed(self):
644 658 """ Called when the down key is pressed. Returns whether to continue
645 659 processing the event.
646 660 """
647 661 end_cursor = self._get_end_cursor()
648 662 if self.textCursor().blockNumber() == end_cursor.blockNumber():
649 663 self.history_next()
650 664 return False
651 665 return True
652 666
653 667 #---------------------------------------------------------------------------
654 668 # 'HistoryConsoleWidget' interface
655 669 #---------------------------------------------------------------------------
656 670
657 671 def history_previous(self):
658 672 """ If possible, set the input buffer to the previous item in the
659 673 history.
660 674 """
661 675 if self._history_index > 0:
662 676 self._history_index -= 1
663 677 self.input_buffer = self._history[self._history_index]
664 678
665 679 def history_next(self):
666 680 """ Set the input buffer to the next item in the history, or a blank
667 681 line if there is no subsequent item.
668 682 """
669 683 if self._history_index < len(self._history):
670 684 self._history_index += 1
671 685 if self._history_index < len(self._history):
672 686 self.input_buffer = self._history[self._history_index]
673 687 else:
674 688 self.input_buffer = ''
@@ -1,276 +1,302 b''
1 1 # System library imports
2 2 from pygments.lexers import PythonLexer
3 3 from PyQt4 import QtCore, QtGui
4 4 import zmq
5 5
6 6 # Local imports
7 7 from IPython.core.blockbreaker import BlockBreaker
8 8 from call_tip_widget import CallTipWidget
9 9 from completion_lexer import CompletionLexer
10 10 from console_widget import HistoryConsoleWidget
11 11 from pygments_highlighter import PygmentsHighlighter
12 12
13 13
14 14 class FrontendHighlighter(PygmentsHighlighter):
15 15 """ A Python PygmentsHighlighter that can be turned on and off and which
16 16 knows about continuation prompts.
17 17 """
18 18
19 19 def __init__(self, frontend):
20 20 PygmentsHighlighter.__init__(self, frontend.document(), PythonLexer())
21 21 self._current_offset = 0
22 22 self._frontend = frontend
23 23 self.highlighting_on = False
24 24
25 25 def highlightBlock(self, qstring):
26 26 """ Highlight a block of text. Reimplemented to highlight selectively.
27 27 """
28 28 if self.highlighting_on:
29 for prompt in (self._frontend._prompt,
30 self._frontend.continuation_prompt):
29 for prompt in (self._frontend._continuation_prompt,
30 self._frontend._prompt):
31 31 if qstring.startsWith(prompt):
32 32 qstring.remove(0, len(prompt))
33 33 self._current_offset = len(prompt)
34 34 break
35 35 PygmentsHighlighter.highlightBlock(self, qstring)
36 36
37 37 def setFormat(self, start, count, format):
38 38 """ Reimplemented to avoid highlighting continuation prompts.
39 39 """
40 40 start += self._current_offset
41 41 PygmentsHighlighter.setFormat(self, start, count, format)
42 42
43 43
44 44 class FrontendWidget(HistoryConsoleWidget):
45 45 """ A Qt frontend for a generic Python kernel.
46 46 """
47 47
48 48 # Emitted when an 'execute_reply' is received from the kernel.
49 49 executed = QtCore.pyqtSignal(object)
50 50
51 51 #---------------------------------------------------------------------------
52 52 # 'QWidget' interface
53 53 #---------------------------------------------------------------------------
54 54
55 def __init__(self, kernel_manager, parent=None):
55 def __init__(self, parent=None):
56 56 super(FrontendWidget, self).__init__(parent)
57 57
58 # ConsoleWidget protected variables.
59 self._continuation_prompt = '... '
60 self._prompt = '>>> '
61
62 # FrontendWidget protected variables.
58 63 self._blockbreaker = BlockBreaker(input_mode='replace')
59 64 self._call_tip_widget = CallTipWidget(self)
60 65 self._completion_lexer = CompletionLexer(PythonLexer())
61 66 self._hidden = True
62 67 self._highlighter = FrontendHighlighter(self)
63 68 self._kernel_manager = None
64 69
65 self.continuation_prompt = '... '
66 self.kernel_manager = kernel_manager
67
68 70 self.document().contentsChange.connect(self._document_contents_change)
69 71
70 72 def focusOutEvent(self, event):
71 73 """ Reimplemented to hide calltips.
72 74 """
73 75 self._call_tip_widget.hide()
74 76 return super(FrontendWidget, self).focusOutEvent(event)
75 77
76 78 def keyPressEvent(self, event):
77 79 """ Reimplemented to hide calltips.
78 80 """
79 81 if event.key() == QtCore.Qt.Key_Escape:
80 82 self._call_tip_widget.hide()
81 83 return super(FrontendWidget, self).keyPressEvent(event)
82 84
83 85 #---------------------------------------------------------------------------
84 86 # 'ConsoleWidget' abstract interface
85 87 #---------------------------------------------------------------------------
86 88
87 89 def _execute(self, interactive):
88 90 """ Called to execute the input buffer. When triggered by an the enter
89 91 key press, 'interactive' is True; otherwise, it is False. Returns
90 92 whether the input buffer was completely processed and a new prompt
91 93 created.
92 94 """
93 95 return self.execute_source(self.input_buffer, interactive=interactive)
94 96
95 97 def _prompt_started_hook(self):
96 98 """ Called immediately after a new prompt is displayed.
97 99 """
98 100 self._highlighter.highlighting_on = True
99 101
100 102 def _prompt_finished_hook(self):
101 103 """ Called immediately after a prompt is finished, i.e. when some input
102 104 will be processed and a new prompt displayed.
103 105 """
104 106 self._highlighter.highlighting_on = False
105 107
106 108 def _tab_pressed(self):
107 109 """ Called when the tab key is pressed. Returns whether to continue
108 110 processing the event.
109 111 """
110 112 self._keep_cursor_in_buffer()
111 113 cursor = self.textCursor()
112 114 if not self._complete():
113 115 cursor.insertText(' ')
114 116 return False
115 117
116 118 #---------------------------------------------------------------------------
117 119 # 'FrontendWidget' interface
118 120 #---------------------------------------------------------------------------
119 121
120 122 def execute_source(self, source, hidden=False, interactive=False):
121 123 """ Execute a string containing Python code. If 'hidden', no output is
122 124 shown. Returns whether the source executed (i.e., returns True only
123 125 if no more input is necessary).
124 126 """
125 127 self._blockbreaker.push(source)
126 128 executed = self._blockbreaker.interactive_block_ready()
127 129 if executed:
128 130 self.kernel_manager.xreq_channel.execute(source)
129 131 self._hidden = hidden
130 132 else:
131 133 self._show_continuation_prompt()
132 134 self.appendPlainText(' ' * self._blockbreaker.indent_spaces)
133 135 return executed
134 136
135 137 def execute_file(self, path, hidden=False):
136 138 """ Attempts to execute file with 'path'. If 'hidden', no output is
137 139 shown.
138 140 """
139 141 self.execute_source('run %s' % path, hidden=hidden)
140 142
141 143 def _get_kernel_manager(self):
142 144 """ Returns the current kernel manager.
143 145 """
144 146 return self._kernel_manager
145 147
146 148 def _set_kernel_manager(self, kernel_manager):
147 """ Sets a new kernel manager, configuring its channels as necessary.
149 """ Disconnect from the current kernel manager (if any) and set a new
150 kernel manager.
148 151 """
149 # Disconnect the old kernel manager.
152 # Disconnect the old kernel manager, if necessary.
150 153 if self._kernel_manager is not None:
154 self._kernel_manager.started_listening.disconnect(
155 self._started_listening)
156 self._kernel_manager.stopped_listening.disconnect(
157 self._stopped_listening)
158
159 # Disconnect the old kernel manager's channels.
151 160 sub = self._kernel_manager.sub_channel
152 161 xreq = self._kernel_manager.xreq_channel
153 162 sub.message_received.disconnect(self._handle_sub)
154 163 xreq.execute_reply.disconnect(self._handle_execute_reply)
155 164 xreq.complete_reply.disconnect(self._handle_complete_reply)
156 165 xreq.object_info_reply.disconnect(self._handle_object_info_reply)
157 166
158 # Connect the new kernel manager.
167 # Set the new kernel manager.
159 168 self._kernel_manager = kernel_manager
169 if kernel_manager is None:
170 return
171
172 # Connect the new kernel manager.
173 kernel_manager.started_listening.connect(self._started_listening)
174 kernel_manager.stopped_listening.connect(self._stopped_listening)
175
176 # Connect the new kernel manager's channels.
160 177 sub = kernel_manager.sub_channel
161 178 xreq = kernel_manager.xreq_channel
162 179 sub.message_received.connect(self._handle_sub)
163 180 xreq.execute_reply.connect(self._handle_execute_reply)
164 181 xreq.complete_reply.connect(self._handle_complete_reply)
165 182 xreq.object_info_reply.connect(self._handle_object_info_reply)
166 183
167 self._show_prompt('>>> ')
184 # Handle the case where the kernel manager started listening before
185 # we connected.
186 if kernel_manager.is_listening:
187 self._started_listening()
168 188
169 189 kernel_manager = property(_get_kernel_manager, _set_kernel_manager)
170 190
171 191 #---------------------------------------------------------------------------
172 192 # 'FrontendWidget' protected interface
173 193 #---------------------------------------------------------------------------
174 194
175 195 def _call_tip(self):
176 196 """ Shows a call tip, if appropriate, at the current cursor location.
177 197 """
178 198 # Decide if it makes sense to show a call tip
179 199 cursor = self.textCursor()
180 200 cursor.movePosition(QtGui.QTextCursor.Left)
181 201 document = self.document()
182 202 if document.characterAt(cursor.position()).toAscii() != '(':
183 203 return False
184 204 context = self._get_context(cursor)
185 205 if not context:
186 206 return False
187 207
188 208 # Send the metadata request to the kernel
189 209 name = '.'.join(context)
190 210 self._calltip_id = self.kernel_manager.xreq_channel.object_info(name)
191 211 self._calltip_pos = self.textCursor().position()
192 212 return True
193 213
194 214 def _complete(self):
195 215 """ Performs completion at the current cursor location.
196 216 """
197 217 # Decide if it makes sense to do completion
198 218 context = self._get_context()
199 219 if not context:
200 220 return False
201 221
202 222 # Send the completion request to the kernel
203 223 text = '.'.join(context)
204 224 self._complete_id = self.kernel_manager.xreq_channel.complete(
205 225 text, self.input_buffer_cursor_line, self.input_buffer)
206 226 self._complete_pos = self.textCursor().position()
207 227 return True
208 228
209 229 def _get_context(self, cursor=None):
210 230 """ Gets the context at the current cursor location.
211 231 """
212 232 if cursor is None:
213 233 cursor = self.textCursor()
214 234 cursor.movePosition(QtGui.QTextCursor.StartOfLine,
215 235 QtGui.QTextCursor.KeepAnchor)
216 236 text = unicode(cursor.selectedText())
217 237 return self._completion_lexer.get_context(text)
218 238
219 239 #------ Signal handlers ----------------------------------------------------
220 240
221 241 def _document_contents_change(self, position, removed, added):
222 242 """ Called whenever the document's content changes. Display a calltip
223 243 if appropriate.
224 244 """
225 245 # Calculate where the cursor should be *after* the change:
226 246 position += added
227 247
228 248 document = self.document()
229 249 if position == self.textCursor().position():
230 250 self._call_tip()
231 251
232 252 def _handle_sub(self, omsg):
233 253 if not self._hidden:
234 254 handler = getattr(self, '_handle_%s' % omsg['msg_type'], None)
235 255 if handler is not None:
236 256 handler(omsg)
237 257
238 258 def _handle_pyout(self, omsg):
239 259 session = omsg['parent_header']['session']
240 260 if session == self.kernel_manager.session.session:
241 261 self.appendPlainText(omsg['content']['data'] + '\n')
242 262
243 263 def _handle_stream(self, omsg):
244 264 self.appendPlainText(omsg['content']['data'])
245 265
246 266 def _handle_execute_reply(self, rep):
247 267 # Make sure that all output from the SUB channel has been processed
248 268 # before writing a new prompt.
249 269 self.kernel_manager.sub_channel.flush()
250 270
251 271 content = rep['content']
252 272 status = content['status']
253 273 if status == 'error':
254 274 self.appendPlainText(content['traceback'][-1])
255 275 elif status == 'aborted':
256 276 text = "ERROR: ABORTED\n"
257 277 self.appendPlainText(text)
258 278 self._hidden = True
259 self._show_prompt('>>> ')
279 self._show_prompt()
260 280 self.executed.emit(rep)
261 281
262 282 def _handle_complete_reply(self, rep):
263 283 cursor = self.textCursor()
264 284 if rep['parent_header']['msg_id'] == self._complete_id and \
265 285 cursor.position() == self._complete_pos:
266 286 text = '.'.join(self._get_context())
267 287 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
268 288 self._complete_with_items(cursor, rep['content']['matches'])
269 289
270 290 def _handle_object_info_reply(self, rep):
271 291 cursor = self.textCursor()
272 292 if rep['parent_header']['msg_id'] == self._calltip_id and \
273 293 cursor.position() == self._calltip_pos:
274 294 doc = rep['content']['docstring']
275 295 if doc:
276 296 self._call_tip_widget.show_tip(doc)
297
298 def _started_listening(self):
299 self.clear()
300
301 def _stopped_listening(self):
302 pass
@@ -1,95 +1,96 b''
1 1 # System library imports
2 2 from PyQt4 import QtCore, QtGui
3 3
4 4 # Local imports
5 5 from frontend_widget import FrontendWidget
6 6
7 7
8 8 class IPythonWidget(FrontendWidget):
9 9 """ A FrontendWidget for an IPython kernel.
10 10 """
11 11
12 12 #---------------------------------------------------------------------------
13 13 # 'FrontendWidget' interface
14 14 #---------------------------------------------------------------------------
15 15
16 def __init__(self, kernel_manager, parent=None):
17 super(IPythonWidget, self).__init__(kernel_manager, parent)
16 def __init__(self, parent=None):
17 super(IPythonWidget, self).__init__(parent)
18 18
19 19 self._magic_overrides = {}
20 20
21 21 def execute_source(self, source, hidden=False, interactive=False):
22 22 """ Reimplemented to override magic commands.
23 23 """
24 24 magic_source = source.strip()
25 25 if magic_source.startswith('%'):
26 26 magic_source = magic_source[1:]
27 27 magic, sep, arguments = magic_source.partition(' ')
28 28 if not magic:
29 29 magic = magic_source
30 30
31 31 callback = self._magic_overrides.get(magic)
32 32 if callback:
33 33 output = callback(arguments)
34 34 if output:
35 35 self.appendPlainText(output)
36 36 self._show_prompt('>>> ')
37 37 return True
38 38 else:
39 39 return super(IPythonWidget, self).execute_source(source, hidden,
40 40 interactive)
41 41
42 42 #---------------------------------------------------------------------------
43 43 # 'IPythonWidget' interface
44 44 #---------------------------------------------------------------------------
45 45
46 46 def set_magic_override(self, magic, callback):
47 47 """ Overrides an IPython magic command. This magic will be intercepted
48 48 by the frontend rather than passed on to the kernel and 'callback'
49 49 will be called with a single argument: a string of argument(s) for
50 50 the magic. The callback can (optionally) return text to print to the
51 51 console.
52 52 """
53 53 self._magic_overrides[magic] = callback
54 54
55 55 def remove_magic_override(self, magic):
56 56 """ Removes the override for the specified magic, if there is one.
57 57 """
58 58 try:
59 59 del self._magic_overrides[magic]
60 60 except KeyError:
61 61 pass
62 62
63 63
64 64 if __name__ == '__main__':
65 65 from IPython.external.argparse import ArgumentParser
66 66 from IPython.frontend.qt.kernelmanager import QtKernelManager
67 67
68 68 # Don't let Qt swallow KeyboardInterupts.
69 69 import signal
70 70 signal.signal(signal.SIGINT, signal.SIG_DFL)
71 71
72 72 # Parse command line arguments.
73 73 parser = ArgumentParser()
74 74 parser.add_argument('--ip', type=str, default='127.0.0.1',
75 75 help='set the kernel\'s IP address [default localhost]')
76 76 parser.add_argument('--xreq', type=int, metavar='PORT', default=5575,
77 77 help='set the XREQ Channel port [default %(default)i]')
78 78 parser.add_argument('--sub', type=int, metavar='PORT', default=5576,
79 79 help='set the SUB Channel port [default %(default)i]')
80 80 namespace = parser.parse_args()
81 81
82 82 # Create KernelManager
83 83 ip = namespace.ip
84 84 kernel_manager = QtKernelManager(xreq_address = (ip, namespace.xreq),
85 85 sub_address = (ip, namespace.sub))
86 86 kernel_manager.start_listening()
87 87
88 88 # Launch application
89 89 app = QtGui.QApplication([])
90 widget = IPythonWidget(kernel_manager)
90 widget = IPythonWidget()
91 widget.kernel_manager = kernel_manager
91 92 widget.setWindowTitle('Python')
92 93 widget.resize(640, 480)
93 94 widget.show()
94 95 app.exec_()
95 96
@@ -1,118 +1,144 b''
1 """ A KernelManager that provides channels that use signals and slots.
1 """ Defines a KernelManager that provides signals and slots.
2 2 """
3 3
4 4 # System library imports.
5 5 from PyQt4 import QtCore
6 6
7 7 # IPython imports.
8 8 from IPython.zmq.kernelmanager import KernelManager, SubSocketChannel, \
9 9 XReqSocketChannel, RepSocketChannel
10 from util import MetaQObjectHasTraits
10 11
11 12
12 13 class QtSubSocketChannel(SubSocketChannel, QtCore.QObject):
13 14
14 15 # Emitted when any message is received.
15 16 message_received = QtCore.pyqtSignal(object)
16 17
17 18 # Emitted when a message of type 'pyout' or 'stdout' is received.
18 19 output_received = QtCore.pyqtSignal(object)
19 20
20 21 # Emitted when a message of type 'pyerr' or 'stderr' is received.
21 22 error_received = QtCore.pyqtSignal(object)
22 23
23 24 #---------------------------------------------------------------------------
24 25 # 'object' interface
25 26 #---------------------------------------------------------------------------
26 27
27 28 def __init__(self, *args, **kw):
28 29 """ Reimplemented to ensure that QtCore.QObject is initialized first.
29 30 """
30 31 QtCore.QObject.__init__(self)
31 32 SubSocketChannel.__init__(self, *args, **kw)
32 33
33 34 #---------------------------------------------------------------------------
34 35 # 'SubSocketChannel' interface
35 36 #---------------------------------------------------------------------------
36 37
37 38 def call_handlers(self, msg):
38 39 """ Reimplemented to emit signals instead of making callbacks.
39 40 """
40 41 # Emit the generic signal.
41 42 self.message_received.emit(msg)
42 43
43 44 # Emit signals for specialized message types.
44 45 msg_type = msg['msg_type']
45 46 if msg_type in ('pyout', 'stdout'):
46 47 self.output_received.emit(msg)
47 48 elif msg_type in ('pyerr', 'stderr'):
48 49 self.error_received.emit(msg)
49 50
50 51 def flush(self):
51 52 """ Reimplemented to ensure that signals are dispatched immediately.
52 53 """
53 54 super(QtSubSocketChannel, self).flush()
54 55 QtCore.QCoreApplication.instance().processEvents()
55 56
56 57
57 58 class QtXReqSocketChannel(XReqSocketChannel, QtCore.QObject):
58 59
59 60 # Emitted when any message is received.
60 61 message_received = QtCore.pyqtSignal(object)
61 62
62 63 # Emitted when a reply has been received for the corresponding request type.
63 64 execute_reply = QtCore.pyqtSignal(object)
64 65 complete_reply = QtCore.pyqtSignal(object)
65 66 object_info_reply = QtCore.pyqtSignal(object)
66 67
67 68 #---------------------------------------------------------------------------
68 69 # 'object' interface
69 70 #---------------------------------------------------------------------------
70 71
71 72 def __init__(self, *args, **kw):
72 73 """ Reimplemented to ensure that QtCore.QObject is initialized first.
73 74 """
74 75 QtCore.QObject.__init__(self)
75 76 XReqSocketChannel.__init__(self, *args, **kw)
76 77
77 78 #---------------------------------------------------------------------------
78 79 # 'XReqSocketChannel' interface
79 80 #---------------------------------------------------------------------------
80 81
81 82 def call_handlers(self, msg):
82 83 """ Reimplemented to emit signals instead of making callbacks.
83 84 """
84 85 # Emit the generic signal.
85 86 self.message_received.emit(msg)
86 87
87 88 # Emit signals for specialized message types.
88 89 msg_type = msg['msg_type']
89 90 signal = getattr(self, msg_type, None)
90 91 if signal:
91 92 signal.emit(msg)
92 93
93 94 def _queue_request(self, msg, callback):
94 95 """ Reimplemented to skip callback handling.
95 96 """
96 97 self.command_queue.put(msg)
97 98
98 99
99 100 class QtRepSocketChannel(RepSocketChannel, QtCore.QObject):
100 101
101 102 #---------------------------------------------------------------------------
102 103 # 'object' interface
103 104 #---------------------------------------------------------------------------
104 105
105 106 def __init__(self, *args, **kw):
106 107 """ Reimplemented to ensure that QtCore.QObject is initialized first.
107 108 """
108 109 QtCore.QObject.__init__(self)
109 110 RepSocketChannel.__init__(self, *args, **kw)
110 111
111 112
112 class QtKernelManager(KernelManager):
113 """ A KernelManager that provides channels that use signals and slots.
113 class QtKernelManager(KernelManager, QtCore.QObject):
114 """ A KernelManager that provides signals and slots.
114 115 """
115 116
117 __metaclass__ = MetaQObjectHasTraits
118
119 # Emitted when the kernel manager has started listening.
120 started_listening = QtCore.pyqtSignal()
121
122 # Emitted when the kernel manager has stopped listening.
123 stopped_listening = QtCore.pyqtSignal()
124
125 # Use Qt-specific channel classes that emit signals.
116 126 sub_channel_class = QtSubSocketChannel
117 127 xreq_channel_class = QtXReqSocketChannel
118 128 rep_channel_class = QtRepSocketChannel
129
130 #---------------------------------------------------------------------------
131 # 'KernelManager' interface
132 #---------------------------------------------------------------------------
133
134 def start_listening(self):
135 """ Reimplemented to emit signal.
136 """
137 super(QtKernelManager, self).start_listening()
138 self.started_listening.emit()
139
140 def stop_listening(self):
141 """ Reimplemented to emit signal.
142 """
143 super(QtKernelManager, self).stop_listening()
144 self.stopped_listening.emit()
General Comments 0
You need to be logged in to leave comments. Login now