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