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