##// END OF EJS Templates
* ConsoleWidget no longer stores contiguous identical lines...
epatters -
Show More
@@ -1,1568 +1,1571 b''
1 1 # Standard library imports
2 2 from os.path import commonprefix
3 3 import re
4 4 import sys
5 5 from textwrap import dedent
6 6
7 7 # System library imports
8 8 from PyQt4 import QtCore, QtGui
9 9
10 10 # Local imports
11 11 from IPython.config.configurable import Configurable
12 12 from IPython.frontend.qt.util import MetaQObjectHasTraits
13 13 from IPython.utils.traitlets import Bool, Enum, Int
14 14 from ansi_code_processor import QtAnsiCodeProcessor
15 15 from completion_widget import CompletionWidget
16 16
17 17
18 18 class ConsolePlainTextEdit(QtGui.QPlainTextEdit):
19 19 """ A QPlainTextEdit suitable for use with ConsoleWidget.
20 20 """
21 21 # Prevents text from being moved by drag and drop. Note that is not, for
22 22 # some reason, sufficient to catch drag events in the ConsoleWidget's
23 23 # event filter.
24 24 def dragEnterEvent(self, event): pass
25 25 def dragLeaveEvent(self, event): pass
26 26 def dragMoveEvent(self, event): pass
27 27 def dropEvent(self, event): pass
28 28
29 29 class ConsoleTextEdit(QtGui.QTextEdit):
30 30 """ A QTextEdit suitable for use with ConsoleWidget.
31 31 """
32 32 # See above.
33 33 def dragEnterEvent(self, event): pass
34 34 def dragLeaveEvent(self, event): pass
35 35 def dragMoveEvent(self, event): pass
36 36 def dropEvent(self, event): pass
37 37
38 38
39 39 class ConsoleWidget(Configurable, QtGui.QWidget):
40 40 """ An abstract base class for console-type widgets. This class has
41 41 functionality for:
42 42
43 43 * Maintaining a prompt and editing region
44 44 * Providing the traditional Unix-style console keyboard shortcuts
45 45 * Performing tab completion
46 46 * Paging text
47 47 * Handling ANSI escape codes
48 48
49 49 ConsoleWidget also provides a number of utility methods that will be
50 50 convenient to implementors of a console-style widget.
51 51 """
52 52 __metaclass__ = MetaQObjectHasTraits
53 53
54 54 # Whether to process ANSI escape codes.
55 55 ansi_codes = Bool(True, config=True)
56 56
57 57 # The maximum number of lines of text before truncation. Specifying a
58 58 # non-positive number disables text truncation (not recommended).
59 59 buffer_size = Int(500, config=True)
60 60
61 61 # Whether to use a list widget or plain text output for tab completion.
62 62 gui_completion = Bool(False, config=True)
63 63
64 64 # The type of underlying text widget to use. Valid values are 'plain', which
65 65 # specifies a QPlainTextEdit, and 'rich', which specifies a QTextEdit.
66 66 # NOTE: this value can only be specified during initialization.
67 67 kind = Enum(['plain', 'rich'], default_value='plain', config=True)
68 68
69 69 # The type of paging to use. Valid values are:
70 70 # 'inside' : The widget pages like a traditional terminal pager.
71 71 # 'hsplit' : When paging is requested, the widget is split
72 72 # horizontally. The top pane contains the console, and the
73 73 # bottom pane contains the paged text.
74 74 # 'vsplit' : Similar to 'hsplit', except that a vertical splitter used.
75 75 # 'custom' : No action is taken by the widget beyond emitting a
76 76 # 'custom_page_requested(str)' signal.
77 77 # 'none' : The text is written directly to the console.
78 78 # NOTE: this value can only be specified during initialization.
79 79 paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
80 80 default_value='inside', config=True)
81 81
82 82 # Whether to override ShortcutEvents for the keybindings defined by this
83 83 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
84 84 # priority (when it has focus) over, e.g., window-level menu shortcuts.
85 85 override_shortcuts = Bool(False)
86 86
87 87 # Signals that indicate ConsoleWidget state.
88 88 copy_available = QtCore.pyqtSignal(bool)
89 89 redo_available = QtCore.pyqtSignal(bool)
90 90 undo_available = QtCore.pyqtSignal(bool)
91 91
92 92 # Signal emitted when paging is needed and the paging style has been
93 93 # specified as 'custom'.
94 94 custom_page_requested = QtCore.pyqtSignal(object)
95 95
96 96 # Protected class variables.
97 97 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
98 98 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
99 99 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
100 100 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
101 101 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
102 102 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
103 103 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
104 104 _shortcuts = set(_ctrl_down_remap.keys() +
105 105 [ QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
106 106 QtCore.Qt.Key_V ])
107 107
108 108 #---------------------------------------------------------------------------
109 109 # 'QObject' interface
110 110 #---------------------------------------------------------------------------
111 111
112 112 def __init__(self, parent=None, **kw):
113 113 """ Create a ConsoleWidget.
114 114
115 115 Parameters:
116 116 -----------
117 117 parent : QWidget, optional [default None]
118 118 The parent for this widget.
119 119 """
120 120 QtGui.QWidget.__init__(self, parent)
121 121 Configurable.__init__(self, **kw)
122 122
123 123 # Create the layout and underlying text widget.
124 124 layout = QtGui.QStackedLayout(self)
125 125 layout.setContentsMargins(0, 0, 0, 0)
126 126 self._control = self._create_control()
127 127 self._page_control = None
128 128 self._splitter = None
129 129 if self.paging in ('hsplit', 'vsplit'):
130 130 self._splitter = QtGui.QSplitter()
131 131 if self.paging == 'hsplit':
132 132 self._splitter.setOrientation(QtCore.Qt.Horizontal)
133 133 else:
134 134 self._splitter.setOrientation(QtCore.Qt.Vertical)
135 135 self._splitter.addWidget(self._control)
136 136 layout.addWidget(self._splitter)
137 137 else:
138 138 layout.addWidget(self._control)
139 139
140 140 # Create the paging widget, if necessary.
141 141 if self.paging in ('inside', 'hsplit', 'vsplit'):
142 142 self._page_control = self._create_page_control()
143 143 if self._splitter:
144 144 self._page_control.hide()
145 145 self._splitter.addWidget(self._page_control)
146 146 else:
147 147 layout.addWidget(self._page_control)
148 148
149 149 # Initialize protected variables. Some variables contain useful state
150 150 # information for subclasses; they should be considered read-only.
151 151 self._ansi_processor = QtAnsiCodeProcessor()
152 152 self._completion_widget = CompletionWidget(self._control)
153 153 self._continuation_prompt = '> '
154 154 self._continuation_prompt_html = None
155 155 self._executing = False
156 156 self._prompt = ''
157 157 self._prompt_html = None
158 158 self._prompt_pos = 0
159 159 self._prompt_sep = ''
160 160 self._reading = False
161 161 self._reading_callback = None
162 162 self._tab_width = 8
163 163 self._text_completing_pos = 0
164 164
165 165 # Set a monospaced font.
166 166 self.reset_font()
167 167
168 168 def eventFilter(self, obj, event):
169 169 """ Reimplemented to ensure a console-like behavior in the underlying
170 170 text widgets.
171 171 """
172 172 etype = event.type()
173 173 if etype == QtCore.QEvent.KeyPress:
174 174
175 175 # Re-map keys for all filtered widgets.
176 176 key = event.key()
177 177 if self._control_key_down(event.modifiers()) and \
178 178 key in self._ctrl_down_remap:
179 179 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
180 180 self._ctrl_down_remap[key],
181 181 QtCore.Qt.NoModifier)
182 182 QtGui.qApp.sendEvent(obj, new_event)
183 183 return True
184 184
185 185 elif obj == self._control:
186 186 return self._event_filter_console_keypress(event)
187 187
188 188 elif obj == self._page_control:
189 189 return self._event_filter_page_keypress(event)
190 190
191 191 # Make middle-click paste safe.
192 192 elif etype == QtCore.QEvent.MouseButtonRelease and \
193 193 event.button() == QtCore.Qt.MidButton and \
194 194 obj == self._control.viewport():
195 195 cursor = self._control.cursorForPosition(event.pos())
196 196 self._control.setTextCursor(cursor)
197 197 self.paste(QtGui.QClipboard.Selection)
198 198 return True
199 199
200 200 # Override shortcuts for all filtered widgets.
201 201 elif etype == QtCore.QEvent.ShortcutOverride and \
202 202 self.override_shortcuts and \
203 203 self._control_key_down(event.modifiers()) and \
204 204 event.key() in self._shortcuts:
205 205 event.accept()
206 206 return False
207 207
208 208 return super(ConsoleWidget, self).eventFilter(obj, event)
209 209
210 210 #---------------------------------------------------------------------------
211 211 # 'QWidget' interface
212 212 #---------------------------------------------------------------------------
213 213
214 214 def sizeHint(self):
215 215 """ Reimplemented to suggest a size that is 80 characters wide and
216 216 25 lines high.
217 217 """
218 218 font_metrics = QtGui.QFontMetrics(self.font)
219 219 margin = (self._control.frameWidth() +
220 220 self._control.document().documentMargin()) * 2
221 221 style = self.style()
222 222 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
223 223
224 224 # Note 1: Despite my best efforts to take the various margins into
225 225 # account, the width is still coming out a bit too small, so we include
226 226 # a fudge factor of one character here.
227 227 # Note 2: QFontMetrics.maxWidth is not used here or anywhere else due
228 228 # to a Qt bug on certain Mac OS systems where it returns 0.
229 229 width = font_metrics.width(' ') * 81 + margin
230 230 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
231 231 if self.paging == 'hsplit':
232 232 width = width * 2 + splitwidth
233 233
234 234 height = font_metrics.height() * 25 + margin
235 235 if self.paging == 'vsplit':
236 236 height = height * 2 + splitwidth
237 237
238 238 return QtCore.QSize(width, height)
239 239
240 240 #---------------------------------------------------------------------------
241 241 # 'ConsoleWidget' public interface
242 242 #---------------------------------------------------------------------------
243 243
244 244 def can_paste(self):
245 245 """ Returns whether text can be pasted from the clipboard.
246 246 """
247 247 # Only accept text that can be ASCII encoded.
248 248 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
249 249 text = QtGui.QApplication.clipboard().text()
250 250 if not text.isEmpty():
251 251 try:
252 252 str(text)
253 253 return True
254 254 except UnicodeEncodeError:
255 255 pass
256 256 return False
257 257
258 258 def clear(self, keep_input=True):
259 259 """ Clear the console, then write a new prompt. If 'keep_input' is set,
260 260 restores the old input buffer when the new prompt is written.
261 261 """
262 262 if keep_input:
263 263 input_buffer = self.input_buffer
264 264 self._control.clear()
265 265 self._show_prompt()
266 266 if keep_input:
267 267 self.input_buffer = input_buffer
268 268
269 269 def copy(self):
270 270 """ Copy the current selected text to the clipboard.
271 271 """
272 272 self._control.copy()
273 273
274 274 def execute(self, source=None, hidden=False, interactive=False):
275 275 """ Executes source or the input buffer, possibly prompting for more
276 276 input.
277 277
278 278 Parameters:
279 279 -----------
280 280 source : str, optional
281 281
282 282 The source to execute. If not specified, the input buffer will be
283 283 used. If specified and 'hidden' is False, the input buffer will be
284 284 replaced with the source before execution.
285 285
286 286 hidden : bool, optional (default False)
287 287
288 288 If set, no output will be shown and the prompt will not be modified.
289 289 In other words, it will be completely invisible to the user that
290 290 an execution has occurred.
291 291
292 292 interactive : bool, optional (default False)
293 293
294 294 Whether the console is to treat the source as having been manually
295 295 entered by the user. The effect of this parameter depends on the
296 296 subclass implementation.
297 297
298 298 Raises:
299 299 -------
300 300 RuntimeError
301 301 If incomplete input is given and 'hidden' is True. In this case,
302 302 it is not possible to prompt for more input.
303 303
304 304 Returns:
305 305 --------
306 306 A boolean indicating whether the source was executed.
307 307 """
308 308 # WARNING: The order in which things happen here is very particular, in
309 309 # large part because our syntax highlighting is fragile. If you change
310 310 # something, test carefully!
311 311
312 312 # Decide what to execute.
313 313 if source is None:
314 314 source = self.input_buffer
315 315 if not hidden:
316 316 # A newline is appended later, but it should be considered part
317 317 # of the input buffer.
318 318 source += '\n'
319 319 elif not hidden:
320 320 self.input_buffer = source
321 321
322 322 # Execute the source or show a continuation prompt if it is incomplete.
323 323 complete = self._is_complete(source, interactive)
324 324 if hidden:
325 325 if complete:
326 326 self._execute(source, hidden)
327 327 else:
328 328 error = 'Incomplete noninteractive input: "%s"'
329 329 raise RuntimeError(error % source)
330 330 else:
331 331 if complete:
332 332 self._append_plain_text('\n')
333 333 self._executing_input_buffer = self.input_buffer
334 334 self._executing = True
335 335 self._prompt_finished()
336 336
337 337 # The maximum block count is only in effect during execution.
338 338 # This ensures that _prompt_pos does not become invalid due to
339 339 # text truncation.
340 340 self._control.document().setMaximumBlockCount(self.buffer_size)
341 341
342 342 # Setting a positive maximum block count will automatically
343 343 # disable the undo/redo history, but just to be safe:
344 344 self._control.setUndoRedoEnabled(False)
345 345
346 346 self._execute(source, hidden)
347 347
348 348 else:
349 349 # Do this inside an edit block so continuation prompts are
350 350 # removed seamlessly via undo/redo.
351 351 cursor = self._get_end_cursor()
352 352 cursor.beginEditBlock()
353 353 cursor.insertText('\n')
354 354 self._insert_continuation_prompt(cursor)
355 355 cursor.endEditBlock()
356 356
357 357 # Do not do this inside the edit block. It works as expected
358 358 # when using a QPlainTextEdit control, but does not have an
359 359 # effect when using a QTextEdit. I believe this is a Qt bug.
360 360 self._control.moveCursor(QtGui.QTextCursor.End)
361 361
362 362 return complete
363 363
364 364 def _get_input_buffer(self):
365 365 """ The text that the user has entered entered at the current prompt.
366 366 """
367 367 # If we're executing, the input buffer may not even exist anymore due to
368 368 # the limit imposed by 'buffer_size'. Therefore, we store it.
369 369 if self._executing:
370 370 return self._executing_input_buffer
371 371
372 372 cursor = self._get_end_cursor()
373 373 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
374 374 input_buffer = str(cursor.selection().toPlainText())
375 375
376 376 # Strip out continuation prompts.
377 377 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
378 378
379 379 def _set_input_buffer(self, string):
380 380 """ Replaces the text in the input buffer with 'string'.
381 381 """
382 382 # For now, it is an error to modify the input buffer during execution.
383 383 if self._executing:
384 384 raise RuntimeError("Cannot change input buffer during execution.")
385 385
386 386 # Remove old text.
387 387 cursor = self._get_end_cursor()
388 388 cursor.beginEditBlock()
389 389 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
390 390 cursor.removeSelectedText()
391 391
392 392 # Insert new text with continuation prompts.
393 393 lines = string.splitlines(True)
394 394 if lines:
395 395 self._append_plain_text(lines[0])
396 396 for i in xrange(1, len(lines)):
397 397 if self._continuation_prompt_html is None:
398 398 self._append_plain_text(self._continuation_prompt)
399 399 else:
400 400 self._append_html(self._continuation_prompt_html)
401 401 self._append_plain_text(lines[i])
402 402 cursor.endEditBlock()
403 403 self._control.moveCursor(QtGui.QTextCursor.End)
404 404
405 405 input_buffer = property(_get_input_buffer, _set_input_buffer)
406 406
407 407 def _get_font(self):
408 408 """ The base font being used by the ConsoleWidget.
409 409 """
410 410 return self._control.document().defaultFont()
411 411
412 412 def _set_font(self, font):
413 413 """ Sets the base font for the ConsoleWidget to the specified QFont.
414 414 """
415 415 font_metrics = QtGui.QFontMetrics(font)
416 416 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
417 417
418 418 self._completion_widget.setFont(font)
419 419 self._control.document().setDefaultFont(font)
420 420 if self._page_control:
421 421 self._page_control.document().setDefaultFont(font)
422 422
423 423 font = property(_get_font, _set_font)
424 424
425 425 def paste(self, mode=QtGui.QClipboard.Clipboard):
426 426 """ Paste the contents of the clipboard into the input region.
427 427
428 428 Parameters:
429 429 -----------
430 430 mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
431 431
432 432 Controls which part of the system clipboard is used. This can be
433 433 used to access the selection clipboard in X11 and the Find buffer
434 434 in Mac OS. By default, the regular clipboard is used.
435 435 """
436 436 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
437 437 try:
438 438 text = str(QtGui.QApplication.clipboard().text(mode))
439 439 except UnicodeEncodeError:
440 440 pass
441 441 else:
442 442 self._insert_plain_text_into_buffer(dedent(text))
443 443
444 444 def print_(self, printer):
445 445 """ Print the contents of the ConsoleWidget to the specified QPrinter.
446 446 """
447 447 self._control.print_(printer)
448 448
449 449 def redo(self):
450 450 """ Redo the last operation. If there is no operation to redo, nothing
451 451 happens.
452 452 """
453 453 self._control.redo()
454 454
455 455 def reset_font(self):
456 456 """ Sets the font to the default fixed-width font for this platform.
457 457 """
458 458 if sys.platform == 'win32':
459 459 # FIXME: we should test whether Consolas is available and use it
460 460 # first if it is. Consolas ships by default from Vista onwards,
461 461 # it's *vastly* more readable and prettier than Courier, and is
462 462 # often installed even on XP systems. So we should first check for
463 463 # it, and only fallback to Courier if absolutely necessary.
464 464 name = 'Courier'
465 465 elif sys.platform == 'darwin':
466 466 name = 'Monaco'
467 467 else:
468 468 name = 'Monospace'
469 469 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
470 470 font.setStyleHint(QtGui.QFont.TypeWriter)
471 471 self._set_font(font)
472 472
473 473 def select_all(self):
474 474 """ Selects all the text in the buffer.
475 475 """
476 476 self._control.selectAll()
477 477
478 478 def _get_tab_width(self):
479 479 """ The width (in terms of space characters) for tab characters.
480 480 """
481 481 return self._tab_width
482 482
483 483 def _set_tab_width(self, tab_width):
484 484 """ Sets the width (in terms of space characters) for tab characters.
485 485 """
486 486 font_metrics = QtGui.QFontMetrics(self.font)
487 487 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
488 488
489 489 self._tab_width = tab_width
490 490
491 491 tab_width = property(_get_tab_width, _set_tab_width)
492 492
493 493 def undo(self):
494 494 """ Undo the last operation. If there is no operation to undo, nothing
495 495 happens.
496 496 """
497 497 self._control.undo()
498 498
499 499 #---------------------------------------------------------------------------
500 500 # 'ConsoleWidget' abstract interface
501 501 #---------------------------------------------------------------------------
502 502
503 503 def _is_complete(self, source, interactive):
504 504 """ Returns whether 'source' can be executed. When triggered by an
505 505 Enter/Return key press, 'interactive' is True; otherwise, it is
506 506 False.
507 507 """
508 508 raise NotImplementedError
509 509
510 510 def _execute(self, source, hidden):
511 511 """ Execute 'source'. If 'hidden', do not show any output.
512 512 """
513 513 raise NotImplementedError
514 514
515 515 def _prompt_started_hook(self):
516 516 """ Called immediately after a new prompt is displayed.
517 517 """
518 518 pass
519 519
520 520 def _prompt_finished_hook(self):
521 521 """ Called immediately after a prompt is finished, i.e. when some input
522 522 will be processed and a new prompt displayed.
523 523 """
524 524 pass
525 525
526 526 def _up_pressed(self):
527 527 """ Called when the up key is pressed. Returns whether to continue
528 528 processing the event.
529 529 """
530 530 return True
531 531
532 532 def _down_pressed(self):
533 533 """ Called when the down key is pressed. Returns whether to continue
534 534 processing the event.
535 535 """
536 536 return True
537 537
538 538 def _tab_pressed(self):
539 539 """ Called when the tab key is pressed. Returns whether to continue
540 540 processing the event.
541 541 """
542 542 return False
543 543
544 544 #--------------------------------------------------------------------------
545 545 # 'ConsoleWidget' protected interface
546 546 #--------------------------------------------------------------------------
547 547
548 548 def _append_html(self, html):
549 549 """ Appends html at the end of the console buffer.
550 550 """
551 551 cursor = self._get_end_cursor()
552 552 self._insert_html(cursor, html)
553 553
554 554 def _append_html_fetching_plain_text(self, html):
555 555 """ Appends 'html', then returns the plain text version of it.
556 556 """
557 557 cursor = self._get_end_cursor()
558 558 return self._insert_html_fetching_plain_text(cursor, html)
559 559
560 560 def _append_plain_text(self, text):
561 561 """ Appends plain text at the end of the console buffer, processing
562 562 ANSI codes if enabled.
563 563 """
564 564 cursor = self._get_end_cursor()
565 565 self._insert_plain_text(cursor, text)
566 566
567 567 def _append_plain_text_keeping_prompt(self, text):
568 568 """ Writes 'text' after the current prompt, then restores the old prompt
569 569 with its old input buffer.
570 570 """
571 571 input_buffer = self.input_buffer
572 572 self._append_plain_text('\n')
573 573 self._prompt_finished()
574 574
575 575 self._append_plain_text(text)
576 576 self._show_prompt()
577 577 self.input_buffer = input_buffer
578 578
579 579 def _cancel_text_completion(self):
580 580 """ If text completion is progress, cancel it.
581 581 """
582 582 if self._text_completing_pos:
583 583 self._clear_temporary_buffer()
584 584 self._text_completing_pos = 0
585 585
586 586 def _clear_temporary_buffer(self):
587 587 """ Clears the "temporary text" buffer, i.e. all the text following
588 588 the prompt region.
589 589 """
590 590 # Select and remove all text below the input buffer.
591 591 cursor = self._get_prompt_cursor()
592 592 prompt = self._continuation_prompt.lstrip()
593 593 while cursor.movePosition(QtGui.QTextCursor.NextBlock):
594 594 temp_cursor = QtGui.QTextCursor(cursor)
595 595 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
596 596 text = str(temp_cursor.selection().toPlainText()).lstrip()
597 597 if not text.startswith(prompt):
598 598 break
599 599 else:
600 600 # We've reached the end of the input buffer and no text follows.
601 601 return
602 602 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
603 603 cursor.movePosition(QtGui.QTextCursor.End,
604 604 QtGui.QTextCursor.KeepAnchor)
605 605 cursor.removeSelectedText()
606 606
607 607 # After doing this, we have no choice but to clear the undo/redo
608 608 # history. Otherwise, the text is not "temporary" at all, because it
609 609 # can be recalled with undo/redo. Unfortunately, Qt does not expose
610 610 # fine-grained control to the undo/redo system.
611 611 if self._control.isUndoRedoEnabled():
612 612 self._control.setUndoRedoEnabled(False)
613 613 self._control.setUndoRedoEnabled(True)
614 614
615 615 def _complete_with_items(self, cursor, items):
616 616 """ Performs completion with 'items' at the specified cursor location.
617 617 """
618 618 self._cancel_text_completion()
619 619
620 620 if len(items) == 1:
621 621 cursor.setPosition(self._control.textCursor().position(),
622 622 QtGui.QTextCursor.KeepAnchor)
623 623 cursor.insertText(items[0])
624 624
625 625 elif len(items) > 1:
626 626 current_pos = self._control.textCursor().position()
627 627 prefix = commonprefix(items)
628 628 if prefix:
629 629 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
630 630 cursor.insertText(prefix)
631 631 current_pos = cursor.position()
632 632
633 633 if self.gui_completion:
634 634 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
635 635 self._completion_widget.show_items(cursor, items)
636 636 else:
637 637 cursor.beginEditBlock()
638 638 self._append_plain_text('\n')
639 639 self._page(self._format_as_columns(items))
640 640 cursor.endEditBlock()
641 641
642 642 cursor.setPosition(current_pos)
643 643 self._control.moveCursor(QtGui.QTextCursor.End)
644 644 self._control.setTextCursor(cursor)
645 645 self._text_completing_pos = current_pos
646 646
647 647 def _control_key_down(self, modifiers, include_command=True):
648 648 """ Given a KeyboardModifiers flags object, return whether the Control
649 649 key is down.
650 650
651 651 Parameters:
652 652 -----------
653 653 include_command : bool, optional (default True)
654 654 Whether to treat the Command key as a (mutually exclusive) synonym
655 655 for Control when in Mac OS.
656 656 """
657 657 # Note that on Mac OS, ControlModifier corresponds to the Command key
658 658 # while MetaModifier corresponds to the Control key.
659 659 if sys.platform == 'darwin':
660 660 down = include_command and (modifiers & QtCore.Qt.ControlModifier)
661 661 return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
662 662 else:
663 663 return bool(modifiers & QtCore.Qt.ControlModifier)
664 664
665 665 def _create_control(self):
666 666 """ Creates and connects the underlying text widget.
667 667 """
668 668 # Create the underlying control.
669 669 if self.kind == 'plain':
670 670 control = ConsolePlainTextEdit()
671 671 elif self.kind == 'rich':
672 672 control = ConsoleTextEdit()
673 673 control.setAcceptRichText(False)
674 674
675 675 # Install event filters. The filter on the viewport is needed for
676 676 # mouse events.
677 677 control.installEventFilter(self)
678 678 control.viewport().installEventFilter(self)
679 679
680 680 # Connect signals.
681 681 control.cursorPositionChanged.connect(self._cursor_position_changed)
682 682 control.customContextMenuRequested.connect(self._show_context_menu)
683 683 control.copyAvailable.connect(self.copy_available)
684 684 control.redoAvailable.connect(self.redo_available)
685 685 control.undoAvailable.connect(self.undo_available)
686 686
687 687 # Configure the control.
688 688 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
689 689 control.setReadOnly(True)
690 690 control.setUndoRedoEnabled(False)
691 691 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
692 692 return control
693 693
694 694 def _create_page_control(self):
695 695 """ Creates and connects the underlying paging widget.
696 696 """
697 697 control = ConsolePlainTextEdit()
698 698 control.installEventFilter(self)
699 699 control.setReadOnly(True)
700 700 control.setUndoRedoEnabled(False)
701 701 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
702 702 return control
703 703
704 704 def _event_filter_console_keypress(self, event):
705 705 """ Filter key events for the underlying text widget to create a
706 706 console-like interface.
707 707 """
708 708 intercepted = False
709 709 cursor = self._control.textCursor()
710 710 position = cursor.position()
711 711 key = event.key()
712 712 ctrl_down = self._control_key_down(event.modifiers())
713 713 alt_down = event.modifiers() & QtCore.Qt.AltModifier
714 714 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
715 715
716 716 if event.matches(QtGui.QKeySequence.Paste):
717 717 # Call our paste instead of the underlying text widget's.
718 718 self.paste()
719 719 intercepted = True
720 720
721 721 elif ctrl_down:
722 722 if key == QtCore.Qt.Key_G:
723 723 self._keyboard_quit()
724 724 intercepted = True
725 725
726 726 elif key == QtCore.Qt.Key_K:
727 727 if self._in_buffer(position):
728 728 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
729 729 QtGui.QTextCursor.KeepAnchor)
730 730 if not cursor.hasSelection():
731 731 # Line deletion (remove continuation prompt)
732 732 cursor.movePosition(QtGui.QTextCursor.NextBlock,
733 733 QtGui.QTextCursor.KeepAnchor)
734 734 cursor.movePosition(QtGui.QTextCursor.Right,
735 735 QtGui.QTextCursor.KeepAnchor,
736 736 len(self._continuation_prompt))
737 737 cursor.removeSelectedText()
738 738 intercepted = True
739 739
740 740 elif key == QtCore.Qt.Key_L:
741 741 # It would be better to simply move the prompt block to the top
742 742 # of the control viewport. QPlainTextEdit has a private method
743 743 # to do this (setTopBlock), but it cannot be duplicated here
744 744 # because it requires access to the QTextControl that underlies
745 745 # both QPlainTextEdit and QTextEdit. In short, this can only be
746 746 # achieved by appending newlines after the prompt, which is a
747 747 # gigantic hack and likely to cause other problems.
748 748 self.clear()
749 749 intercepted = True
750 750
751 751 elif key == QtCore.Qt.Key_O:
752 752 if self._page_control and self._page_control.isVisible():
753 753 self._page_control.setFocus()
754 754 intercept = True
755 755
756 756 elif key == QtCore.Qt.Key_X:
757 757 # FIXME: Instead of disabling cut completely, only allow it
758 758 # when safe.
759 759 intercepted = True
760 760
761 761 elif key == QtCore.Qt.Key_Y:
762 762 self.paste()
763 763 intercepted = True
764 764
765 765 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
766 766 intercepted = True
767 767
768 768 elif alt_down:
769 769 if key == QtCore.Qt.Key_B:
770 770 self._set_cursor(self._get_word_start_cursor(position))
771 771 intercepted = True
772 772
773 773 elif key == QtCore.Qt.Key_F:
774 774 self._set_cursor(self._get_word_end_cursor(position))
775 775 intercepted = True
776 776
777 777 elif key == QtCore.Qt.Key_Backspace:
778 778 cursor = self._get_word_start_cursor(position)
779 779 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
780 780 cursor.removeSelectedText()
781 781 intercepted = True
782 782
783 783 elif key == QtCore.Qt.Key_D:
784 784 cursor = self._get_word_end_cursor(position)
785 785 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
786 786 cursor.removeSelectedText()
787 787 intercepted = True
788 788
789 789 elif key == QtCore.Qt.Key_Delete:
790 790 intercepted = True
791 791
792 792 elif key == QtCore.Qt.Key_Greater:
793 793 self._control.moveCursor(QtGui.QTextCursor.End)
794 794 intercepted = True
795 795
796 796 elif key == QtCore.Qt.Key_Less:
797 797 self._control.setTextCursor(self._get_prompt_cursor())
798 798 intercepted = True
799 799
800 800 else:
801 801 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
802 802 intercepted = True
803 803
804 804 # Special handling when tab completing in text mode.
805 805 self._cancel_text_completion()
806 806
807 807 if self._in_buffer(position):
808 808 if self._reading:
809 809 self._append_plain_text('\n')
810 810 self._reading = False
811 811 if self._reading_callback:
812 812 self._reading_callback()
813 813
814 814 # If there is only whitespace after the cursor, execute.
815 815 # Otherwise, split the line with a continuation prompt.
816 816 elif not self._executing:
817 817 cursor.movePosition(QtGui.QTextCursor.End,
818 818 QtGui.QTextCursor.KeepAnchor)
819 819 at_end = cursor.selectedText().trimmed().isEmpty()
820 820 if at_end or shift_down:
821 821 self.execute(interactive = not shift_down)
822 822 else:
823 823 # Do this inside an edit block for clean undo/redo.
824 824 cursor.beginEditBlock()
825 825 cursor.setPosition(position)
826 826 cursor.insertText('\n')
827 827 self._insert_continuation_prompt(cursor)
828 828 cursor.endEditBlock()
829 829
830 830 # Ensure that the whole input buffer is visible.
831 831 # FIXME: This will not be usable if the input buffer
832 832 # is taller than the console widget.
833 833 self._control.moveCursor(QtGui.QTextCursor.End)
834 834 self._control.setTextCursor(cursor)
835 835
836 836 elif key == QtCore.Qt.Key_Escape:
837 837 self._keyboard_quit()
838 838 intercepted = True
839 839
840 840 elif key == QtCore.Qt.Key_Up:
841 841 if self._reading or not self._up_pressed():
842 842 intercepted = True
843 843 else:
844 844 prompt_line = self._get_prompt_cursor().blockNumber()
845 845 intercepted = cursor.blockNumber() <= prompt_line
846 846
847 847 elif key == QtCore.Qt.Key_Down:
848 848 if self._reading or not self._down_pressed():
849 849 intercepted = True
850 850 else:
851 851 end_line = self._get_end_cursor().blockNumber()
852 852 intercepted = cursor.blockNumber() == end_line
853 853
854 854 elif key == QtCore.Qt.Key_Tab:
855 855 if not self._reading:
856 856 intercepted = not self._tab_pressed()
857 857
858 858 elif key == QtCore.Qt.Key_Left:
859 859 intercepted = not self._in_buffer(position - 1)
860 860
861 861 elif key == QtCore.Qt.Key_Home:
862 862 start_line = cursor.blockNumber()
863 863 if start_line == self._get_prompt_cursor().blockNumber():
864 864 start_pos = self._prompt_pos
865 865 else:
866 866 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
867 867 QtGui.QTextCursor.KeepAnchor)
868 868 start_pos = cursor.position()
869 869 start_pos += len(self._continuation_prompt)
870 870 cursor.setPosition(position)
871 871 if shift_down and self._in_buffer(position):
872 872 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
873 873 else:
874 874 cursor.setPosition(start_pos)
875 875 self._set_cursor(cursor)
876 876 intercepted = True
877 877
878 878 elif key == QtCore.Qt.Key_Backspace:
879 879
880 880 # Line deletion (remove continuation prompt)
881 881 line, col = cursor.blockNumber(), cursor.columnNumber()
882 882 if not self._reading and \
883 883 col == len(self._continuation_prompt) and \
884 884 line > self._get_prompt_cursor().blockNumber():
885 885 cursor.beginEditBlock()
886 886 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
887 887 QtGui.QTextCursor.KeepAnchor)
888 888 cursor.removeSelectedText()
889 889 cursor.deletePreviousChar()
890 890 cursor.endEditBlock()
891 891 intercepted = True
892 892
893 893 # Regular backwards deletion
894 894 else:
895 895 anchor = cursor.anchor()
896 896 if anchor == position:
897 897 intercepted = not self._in_buffer(position - 1)
898 898 else:
899 899 intercepted = not self._in_buffer(min(anchor, position))
900 900
901 901 elif key == QtCore.Qt.Key_Delete:
902 902
903 903 # Line deletion (remove continuation prompt)
904 904 if not self._reading and self._in_buffer(position) and \
905 905 cursor.atBlockEnd() and not cursor.hasSelection():
906 906 cursor.movePosition(QtGui.QTextCursor.NextBlock,
907 907 QtGui.QTextCursor.KeepAnchor)
908 908 cursor.movePosition(QtGui.QTextCursor.Right,
909 909 QtGui.QTextCursor.KeepAnchor,
910 910 len(self._continuation_prompt))
911 911 cursor.removeSelectedText()
912 912 intercepted = True
913 913
914 914 # Regular forwards deletion:
915 915 else:
916 916 anchor = cursor.anchor()
917 917 intercepted = (not self._in_buffer(anchor) or
918 918 not self._in_buffer(position))
919 919
920 920 # Don't move the cursor if control is down to allow copy-paste using
921 921 # the keyboard in any part of the buffer.
922 922 if not ctrl_down:
923 923 self._keep_cursor_in_buffer()
924 924
925 925 return intercepted
926 926
927 927 def _event_filter_page_keypress(self, event):
928 928 """ Filter key events for the paging widget to create console-like
929 929 interface.
930 930 """
931 931 key = event.key()
932 932 ctrl_down = self._control_key_down(event.modifiers())
933 933 alt_down = event.modifiers() & QtCore.Qt.AltModifier
934 934
935 935 if ctrl_down:
936 936 if key == QtCore.Qt.Key_O:
937 937 self._control.setFocus()
938 938 intercept = True
939 939
940 940 elif alt_down:
941 941 if key == QtCore.Qt.Key_Greater:
942 942 self._page_control.moveCursor(QtGui.QTextCursor.End)
943 943 intercepted = True
944 944
945 945 elif key == QtCore.Qt.Key_Less:
946 946 self._page_control.moveCursor(QtGui.QTextCursor.Start)
947 947 intercepted = True
948 948
949 949 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
950 950 if self._splitter:
951 951 self._page_control.hide()
952 952 else:
953 953 self.layout().setCurrentWidget(self._control)
954 954 return True
955 955
956 956 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
957 957 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
958 958 QtCore.Qt.Key_PageDown,
959 959 QtCore.Qt.NoModifier)
960 960 QtGui.qApp.sendEvent(self._page_control, new_event)
961 961 return True
962 962
963 963 elif key == QtCore.Qt.Key_Backspace:
964 964 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
965 965 QtCore.Qt.Key_PageUp,
966 966 QtCore.Qt.NoModifier)
967 967 QtGui.qApp.sendEvent(self._page_control, new_event)
968 968 return True
969 969
970 970 return False
971 971
972 972 def _format_as_columns(self, items, separator=' '):
973 973 """ Transform a list of strings into a single string with columns.
974 974
975 975 Parameters
976 976 ----------
977 977 items : sequence of strings
978 978 The strings to process.
979 979
980 980 separator : str, optional [default is two spaces]
981 981 The string that separates columns.
982 982
983 983 Returns
984 984 -------
985 985 The formatted string.
986 986 """
987 987 # Note: this code is adapted from columnize 0.3.2.
988 988 # See http://code.google.com/p/pycolumnize/
989 989
990 990 # Calculate the number of characters available.
991 991 width = self._control.viewport().width()
992 992 char_width = QtGui.QFontMetrics(self.font).width(' ')
993 993 displaywidth = max(10, (width / char_width) - 1)
994 994
995 995 # Some degenerate cases.
996 996 size = len(items)
997 997 if size == 0:
998 998 return '\n'
999 999 elif size == 1:
1000 1000 return '%s\n' % str(items[0])
1001 1001
1002 1002 # Try every row count from 1 upwards
1003 1003 array_index = lambda nrows, row, col: nrows*col + row
1004 1004 for nrows in range(1, size):
1005 1005 ncols = (size + nrows - 1) // nrows
1006 1006 colwidths = []
1007 1007 totwidth = -len(separator)
1008 1008 for col in range(ncols):
1009 1009 # Get max column width for this column
1010 1010 colwidth = 0
1011 1011 for row in range(nrows):
1012 1012 i = array_index(nrows, row, col)
1013 1013 if i >= size: break
1014 1014 x = items[i]
1015 1015 colwidth = max(colwidth, len(x))
1016 1016 colwidths.append(colwidth)
1017 1017 totwidth += colwidth + len(separator)
1018 1018 if totwidth > displaywidth:
1019 1019 break
1020 1020 if totwidth <= displaywidth:
1021 1021 break
1022 1022
1023 1023 # The smallest number of rows computed and the max widths for each
1024 1024 # column has been obtained. Now we just have to format each of the rows.
1025 1025 string = ''
1026 1026 for row in range(nrows):
1027 1027 texts = []
1028 1028 for col in range(ncols):
1029 1029 i = row + nrows*col
1030 1030 if i >= size:
1031 1031 texts.append('')
1032 1032 else:
1033 1033 texts.append(items[i])
1034 1034 while texts and not texts[-1]:
1035 1035 del texts[-1]
1036 1036 for col in range(len(texts)):
1037 1037 texts[col] = texts[col].ljust(colwidths[col])
1038 1038 string += '%s\n' % str(separator.join(texts))
1039 1039 return string
1040 1040
1041 1041 def _get_block_plain_text(self, block):
1042 1042 """ Given a QTextBlock, return its unformatted text.
1043 1043 """
1044 1044 cursor = QtGui.QTextCursor(block)
1045 1045 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1046 1046 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1047 1047 QtGui.QTextCursor.KeepAnchor)
1048 1048 return str(cursor.selection().toPlainText())
1049 1049
1050 1050 def _get_cursor(self):
1051 1051 """ Convenience method that returns a cursor for the current position.
1052 1052 """
1053 1053 return self._control.textCursor()
1054 1054
1055 1055 def _get_end_cursor(self):
1056 1056 """ Convenience method that returns a cursor for the last character.
1057 1057 """
1058 1058 cursor = self._control.textCursor()
1059 1059 cursor.movePosition(QtGui.QTextCursor.End)
1060 1060 return cursor
1061 1061
1062 1062 def _get_input_buffer_cursor_column(self):
1063 1063 """ Returns the column of the cursor in the input buffer, excluding the
1064 1064 contribution by the prompt, or -1 if there is no such column.
1065 1065 """
1066 1066 prompt = self._get_input_buffer_cursor_prompt()
1067 1067 if prompt is None:
1068 1068 return -1
1069 1069 else:
1070 1070 cursor = self._control.textCursor()
1071 1071 return cursor.columnNumber() - len(prompt)
1072 1072
1073 1073 def _get_input_buffer_cursor_line(self):
1074 1074 """ Returns line of the input buffer that contains the cursor, or None
1075 1075 if there is no such line.
1076 1076 """
1077 1077 prompt = self._get_input_buffer_cursor_prompt()
1078 1078 if prompt is None:
1079 1079 return None
1080 1080 else:
1081 1081 cursor = self._control.textCursor()
1082 1082 text = self._get_block_plain_text(cursor.block())
1083 1083 return text[len(prompt):]
1084 1084
1085 1085 def _get_input_buffer_cursor_prompt(self):
1086 1086 """ Returns the (plain text) prompt for line of the input buffer that
1087 1087 contains the cursor, or None if there is no such line.
1088 1088 """
1089 1089 if self._executing:
1090 1090 return None
1091 1091 cursor = self._control.textCursor()
1092 1092 if cursor.position() >= self._prompt_pos:
1093 1093 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1094 1094 return self._prompt
1095 1095 else:
1096 1096 return self._continuation_prompt
1097 1097 else:
1098 1098 return None
1099 1099
1100 1100 def _get_prompt_cursor(self):
1101 1101 """ Convenience method that returns a cursor for the prompt position.
1102 1102 """
1103 1103 cursor = self._control.textCursor()
1104 1104 cursor.setPosition(self._prompt_pos)
1105 1105 return cursor
1106 1106
1107 1107 def _get_selection_cursor(self, start, end):
1108 1108 """ Convenience method that returns a cursor with text selected between
1109 1109 the positions 'start' and 'end'.
1110 1110 """
1111 1111 cursor = self._control.textCursor()
1112 1112 cursor.setPosition(start)
1113 1113 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1114 1114 return cursor
1115 1115
1116 1116 def _get_word_start_cursor(self, position):
1117 1117 """ Find the start of the word to the left the given position. If a
1118 1118 sequence of non-word characters precedes the first word, skip over
1119 1119 them. (This emulates the behavior of bash, emacs, etc.)
1120 1120 """
1121 1121 document = self._control.document()
1122 1122 position -= 1
1123 1123 while position >= self._prompt_pos and \
1124 1124 not document.characterAt(position).isLetterOrNumber():
1125 1125 position -= 1
1126 1126 while position >= self._prompt_pos and \
1127 1127 document.characterAt(position).isLetterOrNumber():
1128 1128 position -= 1
1129 1129 cursor = self._control.textCursor()
1130 1130 cursor.setPosition(position + 1)
1131 1131 return cursor
1132 1132
1133 1133 def _get_word_end_cursor(self, position):
1134 1134 """ Find the end of the word to the right the given position. If a
1135 1135 sequence of non-word characters precedes the first word, skip over
1136 1136 them. (This emulates the behavior of bash, emacs, etc.)
1137 1137 """
1138 1138 document = self._control.document()
1139 1139 end = self._get_end_cursor().position()
1140 1140 while position < end and \
1141 1141 not document.characterAt(position).isLetterOrNumber():
1142 1142 position += 1
1143 1143 while position < end and \
1144 1144 document.characterAt(position).isLetterOrNumber():
1145 1145 position += 1
1146 1146 cursor = self._control.textCursor()
1147 1147 cursor.setPosition(position)
1148 1148 return cursor
1149 1149
1150 1150 def _insert_continuation_prompt(self, cursor):
1151 1151 """ Inserts new continuation prompt using the specified cursor.
1152 1152 """
1153 1153 if self._continuation_prompt_html is None:
1154 1154 self._insert_plain_text(cursor, self._continuation_prompt)
1155 1155 else:
1156 1156 self._continuation_prompt = self._insert_html_fetching_plain_text(
1157 1157 cursor, self._continuation_prompt_html)
1158 1158
1159 1159 def _insert_html(self, cursor, html):
1160 1160 """ Inserts HTML using the specified cursor in such a way that future
1161 1161 formatting is unaffected.
1162 1162 """
1163 1163 cursor.beginEditBlock()
1164 1164 cursor.insertHtml(html)
1165 1165
1166 1166 # After inserting HTML, the text document "remembers" it's in "html
1167 1167 # mode", which means that subsequent calls adding plain text will result
1168 1168 # in unwanted formatting, lost tab characters, etc. The following code
1169 1169 # hacks around this behavior, which I consider to be a bug in Qt, by
1170 1170 # (crudely) resetting the document's style state.
1171 1171 cursor.movePosition(QtGui.QTextCursor.Left,
1172 1172 QtGui.QTextCursor.KeepAnchor)
1173 1173 if cursor.selection().toPlainText() == ' ':
1174 1174 cursor.removeSelectedText()
1175 1175 else:
1176 1176 cursor.movePosition(QtGui.QTextCursor.Right)
1177 1177 cursor.insertText(' ', QtGui.QTextCharFormat())
1178 1178 cursor.endEditBlock()
1179 1179
1180 1180 def _insert_html_fetching_plain_text(self, cursor, html):
1181 1181 """ Inserts HTML using the specified cursor, then returns its plain text
1182 1182 version.
1183 1183 """
1184 1184 cursor.beginEditBlock()
1185 1185 cursor.removeSelectedText()
1186 1186
1187 1187 start = cursor.position()
1188 1188 self._insert_html(cursor, html)
1189 1189 end = cursor.position()
1190 1190 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1191 1191 text = str(cursor.selection().toPlainText())
1192 1192
1193 1193 cursor.setPosition(end)
1194 1194 cursor.endEditBlock()
1195 1195 return text
1196 1196
1197 1197 def _insert_plain_text(self, cursor, text):
1198 1198 """ Inserts plain text using the specified cursor, processing ANSI codes
1199 1199 if enabled.
1200 1200 """
1201 1201 cursor.beginEditBlock()
1202 1202 if self.ansi_codes:
1203 1203 for substring in self._ansi_processor.split_string(text):
1204 1204 for action in self._ansi_processor.actions:
1205 1205 if action.kind == 'erase' and action.area == 'screen':
1206 1206 cursor.select(QtGui.QTextCursor.Document)
1207 1207 cursor.removeSelectedText()
1208 1208 format = self._ansi_processor.get_format()
1209 1209 cursor.insertText(substring, format)
1210 1210 else:
1211 1211 cursor.insertText(text)
1212 1212 cursor.endEditBlock()
1213 1213
1214 1214 def _insert_plain_text_into_buffer(self, text):
1215 1215 """ Inserts text into the input buffer at the current cursor position,
1216 1216 ensuring that continuation prompts are inserted as necessary.
1217 1217 """
1218 1218 lines = str(text).splitlines(True)
1219 1219 if lines:
1220 1220 self._keep_cursor_in_buffer()
1221 1221 cursor = self._control.textCursor()
1222 1222 cursor.beginEditBlock()
1223 1223 cursor.insertText(lines[0])
1224 1224 for line in lines[1:]:
1225 1225 if self._continuation_prompt_html is None:
1226 1226 cursor.insertText(self._continuation_prompt)
1227 1227 else:
1228 1228 self._continuation_prompt = \
1229 1229 self._insert_html_fetching_plain_text(
1230 1230 cursor, self._continuation_prompt_html)
1231 1231 cursor.insertText(line)
1232 1232 cursor.endEditBlock()
1233 1233 self._control.setTextCursor(cursor)
1234 1234
1235 1235 def _in_buffer(self, position=None):
1236 1236 """ Returns whether the current cursor (or, if specified, a position) is
1237 1237 inside the editing region.
1238 1238 """
1239 1239 cursor = self._control.textCursor()
1240 1240 if position is None:
1241 1241 position = cursor.position()
1242 1242 else:
1243 1243 cursor.setPosition(position)
1244 1244 line = cursor.blockNumber()
1245 1245 prompt_line = self._get_prompt_cursor().blockNumber()
1246 1246 if line == prompt_line:
1247 1247 return position >= self._prompt_pos
1248 1248 elif line > prompt_line:
1249 1249 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1250 1250 prompt_pos = cursor.position() + len(self._continuation_prompt)
1251 1251 return position >= prompt_pos
1252 1252 return False
1253 1253
1254 1254 def _keep_cursor_in_buffer(self):
1255 1255 """ Ensures that the cursor is inside the editing region. Returns
1256 1256 whether the cursor was moved.
1257 1257 """
1258 1258 moved = not self._in_buffer()
1259 1259 if moved:
1260 1260 cursor = self._control.textCursor()
1261 1261 cursor.movePosition(QtGui.QTextCursor.End)
1262 1262 self._control.setTextCursor(cursor)
1263 1263 return moved
1264 1264
1265 1265 def _keyboard_quit(self):
1266 1266 """ Cancels the current editing task ala Ctrl-G in Emacs.
1267 1267 """
1268 1268 if self._text_completing_pos:
1269 1269 self._cancel_text_completion()
1270 1270 else:
1271 1271 self.input_buffer = ''
1272 1272
1273 1273 def _page(self, text):
1274 1274 """ Displays text using the pager if it exceeds the height of the
1275 1275 visible area.
1276 1276 """
1277 1277 if self.paging == 'none':
1278 1278 self._append_plain_text(text)
1279 1279 else:
1280 1280 line_height = QtGui.QFontMetrics(self.font).height()
1281 1281 minlines = self._control.viewport().height() / line_height
1282 1282 if re.match("(?:[^\n]*\n){%i}" % minlines, text):
1283 1283 if self.paging == 'custom':
1284 1284 self.custom_page_requested.emit(text)
1285 1285 else:
1286 1286 self._page_control.clear()
1287 1287 cursor = self._page_control.textCursor()
1288 1288 self._insert_plain_text(cursor, text)
1289 1289 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1290 1290
1291 1291 self._page_control.viewport().resize(self._control.size())
1292 1292 if self._splitter:
1293 1293 self._page_control.show()
1294 1294 self._page_control.setFocus()
1295 1295 else:
1296 1296 self.layout().setCurrentWidget(self._page_control)
1297 1297 else:
1298 1298 self._append_plain_text(text)
1299 1299
1300 1300 def _prompt_started(self):
1301 1301 """ Called immediately after a new prompt is displayed.
1302 1302 """
1303 1303 # Temporarily disable the maximum block count to permit undo/redo and
1304 1304 # to ensure that the prompt position does not change due to truncation.
1305 1305 # Because setting this property clears the undo/redo history, we only
1306 1306 # set it if we have to.
1307 1307 if self._control.document().maximumBlockCount() > 0:
1308 1308 self._control.document().setMaximumBlockCount(0)
1309 1309 self._control.setUndoRedoEnabled(True)
1310 1310
1311 1311 self._control.setReadOnly(False)
1312 1312 self._control.moveCursor(QtGui.QTextCursor.End)
1313 1313
1314 1314 self._executing = False
1315 1315 self._prompt_started_hook()
1316 1316
1317 1317 def _prompt_finished(self):
1318 1318 """ Called immediately after a prompt is finished, i.e. when some input
1319 1319 will be processed and a new prompt displayed.
1320 1320 """
1321 1321 self._control.setReadOnly(True)
1322 1322 self._prompt_finished_hook()
1323 1323
1324 1324 def _readline(self, prompt='', callback=None):
1325 1325 """ Reads one line of input from the user.
1326 1326
1327 1327 Parameters
1328 1328 ----------
1329 1329 prompt : str, optional
1330 1330 The prompt to print before reading the line.
1331 1331
1332 1332 callback : callable, optional
1333 1333 A callback to execute with the read line. If not specified, input is
1334 1334 read *synchronously* and this method does not return until it has
1335 1335 been read.
1336 1336
1337 1337 Returns
1338 1338 -------
1339 1339 If a callback is specified, returns nothing. Otherwise, returns the
1340 1340 input string with the trailing newline stripped.
1341 1341 """
1342 1342 if self._reading:
1343 1343 raise RuntimeError('Cannot read a line. Widget is already reading.')
1344 1344
1345 1345 if not callback and not self.isVisible():
1346 1346 # If the user cannot see the widget, this function cannot return.
1347 1347 raise RuntimeError('Cannot synchronously read a line if the widget '
1348 1348 'is not visible!')
1349 1349
1350 1350 self._reading = True
1351 1351 self._show_prompt(prompt, newline=False)
1352 1352
1353 1353 if callback is None:
1354 1354 self._reading_callback = None
1355 1355 while self._reading:
1356 1356 QtCore.QCoreApplication.processEvents()
1357 1357 return self.input_buffer.rstrip('\n')
1358 1358
1359 1359 else:
1360 1360 self._reading_callback = lambda: \
1361 1361 callback(self.input_buffer.rstrip('\n'))
1362 1362
1363 1363 def _set_continuation_prompt(self, prompt, html=False):
1364 1364 """ Sets the continuation prompt.
1365 1365
1366 1366 Parameters
1367 1367 ----------
1368 1368 prompt : str
1369 1369 The prompt to show when more input is needed.
1370 1370
1371 1371 html : bool, optional (default False)
1372 1372 If set, the prompt will be inserted as formatted HTML. Otherwise,
1373 1373 the prompt will be treated as plain text, though ANSI color codes
1374 1374 will be handled.
1375 1375 """
1376 1376 if html:
1377 1377 self._continuation_prompt_html = prompt
1378 1378 else:
1379 1379 self._continuation_prompt = prompt
1380 1380 self._continuation_prompt_html = None
1381 1381
1382 1382 def _set_cursor(self, cursor):
1383 1383 """ Convenience method to set the current cursor.
1384 1384 """
1385 1385 self._control.setTextCursor(cursor)
1386 1386
1387 1387 def _show_context_menu(self, pos):
1388 1388 """ Shows a context menu at the given QPoint (in widget coordinates).
1389 1389 """
1390 1390 menu = QtGui.QMenu()
1391 1391
1392 1392 copy_action = menu.addAction('Copy', self.copy)
1393 1393 copy_action.setEnabled(self._get_cursor().hasSelection())
1394 1394 copy_action.setShortcut(QtGui.QKeySequence.Copy)
1395 1395
1396 1396 paste_action = menu.addAction('Paste', self.paste)
1397 1397 paste_action.setEnabled(self.can_paste())
1398 1398 paste_action.setShortcut(QtGui.QKeySequence.Paste)
1399 1399
1400 1400 menu.addSeparator()
1401 1401 menu.addAction('Select All', self.select_all)
1402 1402
1403 1403 menu.exec_(self._control.mapToGlobal(pos))
1404 1404
1405 1405 def _show_prompt(self, prompt=None, html=False, newline=True):
1406 1406 """ Writes a new prompt at the end of the buffer.
1407 1407
1408 1408 Parameters
1409 1409 ----------
1410 1410 prompt : str, optional
1411 1411 The prompt to show. If not specified, the previous prompt is used.
1412 1412
1413 1413 html : bool, optional (default False)
1414 1414 Only relevant when a prompt is specified. If set, the prompt will
1415 1415 be inserted as formatted HTML. Otherwise, the prompt will be treated
1416 1416 as plain text, though ANSI color codes will be handled.
1417 1417
1418 1418 newline : bool, optional (default True)
1419 1419 If set, a new line will be written before showing the prompt if
1420 1420 there is not already a newline at the end of the buffer.
1421 1421 """
1422 1422 # Insert a preliminary newline, if necessary.
1423 1423 if newline:
1424 1424 cursor = self._get_end_cursor()
1425 1425 if cursor.position() > 0:
1426 1426 cursor.movePosition(QtGui.QTextCursor.Left,
1427 1427 QtGui.QTextCursor.KeepAnchor)
1428 1428 if str(cursor.selection().toPlainText()) != '\n':
1429 1429 self._append_plain_text('\n')
1430 1430
1431 1431 # Write the prompt.
1432 1432 self._append_plain_text(self._prompt_sep)
1433 1433 if prompt is None:
1434 1434 if self._prompt_html is None:
1435 1435 self._append_plain_text(self._prompt)
1436 1436 else:
1437 1437 self._append_html(self._prompt_html)
1438 1438 else:
1439 1439 if html:
1440 1440 self._prompt = self._append_html_fetching_plain_text(prompt)
1441 1441 self._prompt_html = prompt
1442 1442 else:
1443 1443 self._append_plain_text(prompt)
1444 1444 self._prompt = prompt
1445 1445 self._prompt_html = None
1446 1446
1447 1447 self._prompt_pos = self._get_end_cursor().position()
1448 1448 self._prompt_started()
1449 1449
1450 1450 #------ Signal handlers ----------------------------------------------------
1451 1451
1452 1452 def _cursor_position_changed(self):
1453 1453 """ Clears the temporary buffer based on the cursor position.
1454 1454 """
1455 1455 if self._text_completing_pos:
1456 1456 document = self._control.document()
1457 1457 if self._text_completing_pos < document.characterCount():
1458 1458 cursor = self._control.textCursor()
1459 1459 pos = cursor.position()
1460 1460 text_cursor = self._control.textCursor()
1461 1461 text_cursor.setPosition(self._text_completing_pos)
1462 1462 if pos < self._text_completing_pos or \
1463 1463 cursor.blockNumber() > text_cursor.blockNumber():
1464 1464 self._clear_temporary_buffer()
1465 1465 self._text_completing_pos = 0
1466 1466 else:
1467 1467 self._clear_temporary_buffer()
1468 1468 self._text_completing_pos = 0
1469 1469
1470 1470
1471 1471 class HistoryConsoleWidget(ConsoleWidget):
1472 1472 """ A ConsoleWidget that keeps a history of the commands that have been
1473 1473 executed.
1474 1474 """
1475 1475
1476 1476 #---------------------------------------------------------------------------
1477 1477 # 'object' interface
1478 1478 #---------------------------------------------------------------------------
1479 1479
1480 1480 def __init__(self, *args, **kw):
1481 1481 super(HistoryConsoleWidget, self).__init__(*args, **kw)
1482 1482 self._history = []
1483 1483 self._history_index = 0
1484 1484
1485 1485 #---------------------------------------------------------------------------
1486 1486 # 'ConsoleWidget' public interface
1487 1487 #---------------------------------------------------------------------------
1488 1488
1489 1489 def execute(self, source=None, hidden=False, interactive=False):
1490 1490 """ Reimplemented to the store history.
1491 1491 """
1492 1492 if not hidden:
1493 1493 history = self.input_buffer if source is None else source
1494 1494
1495 1495 executed = super(HistoryConsoleWidget, self).execute(
1496 1496 source, hidden, interactive)
1497 1497
1498 1498 if executed and not hidden:
1499 # Save the command unless it was a blank line.
1499 # Save the command unless it was an empty string or was identical
1500 # to the previous command.
1500 1501 history = history.rstrip()
1501 if history:
1502 if history and (not self._history or self._history[-1] != history):
1502 1503 self._history.append(history)
1503 self._history_index = len(self._history)
1504
1505 # Move the history index to the most recent item.
1506 self._history_index = len(self._history)
1504 1507
1505 1508 return executed
1506 1509
1507 1510 #---------------------------------------------------------------------------
1508 1511 # 'ConsoleWidget' abstract interface
1509 1512 #---------------------------------------------------------------------------
1510 1513
1511 1514 def _up_pressed(self):
1512 1515 """ Called when the up key is pressed. Returns whether to continue
1513 1516 processing the event.
1514 1517 """
1515 1518 prompt_cursor = self._get_prompt_cursor()
1516 1519 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
1517 1520 self.history_previous()
1518 1521
1519 1522 # Go to the first line of prompt for seemless history scrolling.
1520 1523 cursor = self._get_prompt_cursor()
1521 1524 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
1522 1525 self._set_cursor(cursor)
1523 1526
1524 1527 return False
1525 1528 return True
1526 1529
1527 1530 def _down_pressed(self):
1528 1531 """ Called when the down key is pressed. Returns whether to continue
1529 1532 processing the event.
1530 1533 """
1531 1534 end_cursor = self._get_end_cursor()
1532 1535 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
1533 1536 self.history_next()
1534 1537 return False
1535 1538 return True
1536 1539
1537 1540 #---------------------------------------------------------------------------
1538 1541 # 'HistoryConsoleWidget' public interface
1539 1542 #---------------------------------------------------------------------------
1540 1543
1541 1544 def history_previous(self):
1542 1545 """ If possible, set the input buffer to the previous item in the
1543 1546 history.
1544 1547 """
1545 1548 if self._history_index > 0:
1546 1549 self._history_index -= 1
1547 1550 self.input_buffer = self._history[self._history_index]
1548 1551
1549 1552 def history_next(self):
1550 1553 """ Set the input buffer to the next item in the history, or a blank
1551 1554 line if there is no subsequent item.
1552 1555 """
1553 1556 if self._history_index < len(self._history):
1554 1557 self._history_index += 1
1555 1558 if self._history_index < len(self._history):
1556 1559 self.input_buffer = self._history[self._history_index]
1557 1560 else:
1558 1561 self.input_buffer = ''
1559 1562
1560 1563 #---------------------------------------------------------------------------
1561 1564 # 'HistoryConsoleWidget' protected interface
1562 1565 #---------------------------------------------------------------------------
1563 1566
1564 1567 def _set_history(self, history):
1565 1568 """ Replace the current history with a sequence of history items.
1566 1569 """
1567 1570 self._history = list(history)
1568 1571 self._history_index = len(self._history)
@@ -1,467 +1,468 b''
1 1 # Standard library imports
2 2 from collections import namedtuple
3 3 import signal
4 4 import sys
5 5
6 6 # System library imports
7 7 from pygments.lexers import PythonLexer
8 8 from PyQt4 import QtCore, QtGui
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 from IPython.utils.io import raw_print
13 14 from IPython.utils.traitlets import Bool
14 15 from bracket_matcher import BracketMatcher
15 16 from call_tip_widget import CallTipWidget
16 17 from completion_lexer import CompletionLexer
17 18 from console_widget import HistoryConsoleWidget
18 19 from pygments_highlighter import PygmentsHighlighter
19 20
20 21
21 22 class FrontendHighlighter(PygmentsHighlighter):
22 23 """ A PygmentsHighlighter that can be turned on and off and that ignores
23 24 prompts.
24 25 """
25 26
26 27 def __init__(self, frontend):
27 28 super(FrontendHighlighter, self).__init__(frontend._control.document())
28 29 self._current_offset = 0
29 30 self._frontend = frontend
30 31 self.highlighting_on = False
31 32
32 33 def highlightBlock(self, qstring):
33 34 """ Highlight a block of text. Reimplemented to highlight selectively.
34 35 """
35 36 if not self.highlighting_on:
36 37 return
37 38
38 39 # The input to this function is unicode string that may contain
39 40 # paragraph break characters, non-breaking spaces, etc. Here we acquire
40 41 # the string as plain text so we can compare it.
41 42 current_block = self.currentBlock()
42 43 string = self._frontend._get_block_plain_text(current_block)
43 44
44 45 # Decide whether to check for the regular or continuation prompt.
45 46 if current_block.contains(self._frontend._prompt_pos):
46 47 prompt = self._frontend._prompt
47 48 else:
48 49 prompt = self._frontend._continuation_prompt
49 50
50 51 # Don't highlight the part of the string that contains the prompt.
51 52 if string.startswith(prompt):
52 53 self._current_offset = len(prompt)
53 54 qstring.remove(0, len(prompt))
54 55 else:
55 56 self._current_offset = 0
56 57
57 58 PygmentsHighlighter.highlightBlock(self, qstring)
58 59
59 60 def rehighlightBlock(self, block):
60 61 """ Reimplemented to temporarily enable highlighting if disabled.
61 62 """
62 63 old = self.highlighting_on
63 64 self.highlighting_on = True
64 65 super(FrontendHighlighter, self).rehighlightBlock(block)
65 66 self.highlighting_on = old
66 67
67 68 def setFormat(self, start, count, format):
68 69 """ Reimplemented to highlight selectively.
69 70 """
70 71 start += self._current_offset
71 72 PygmentsHighlighter.setFormat(self, start, count, format)
72 73
73 74
74 75 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
75 76 """ A Qt frontend for a generic Python kernel.
76 77 """
77 78
78 79 # An option and corresponding signal for overriding the default kernel
79 80 # interrupt behavior.
80 81 custom_interrupt = Bool(False)
81 82 custom_interrupt_requested = QtCore.pyqtSignal()
82 83
83 84 # An option and corresponding signals for overriding the default kernel
84 85 # restart behavior.
85 86 custom_restart = Bool(False)
86 87 custom_restart_kernel_died = QtCore.pyqtSignal(float)
87 88 custom_restart_requested = QtCore.pyqtSignal()
88 89
89 90 # Emitted when an 'execute_reply' has been received from the kernel and
90 91 # processed by the FrontendWidget.
91 92 executed = QtCore.pyqtSignal(object)
92 93
93 94 # Emitted when an exit request has been received from the kernel.
94 95 exit_requested = QtCore.pyqtSignal()
95 96
96 97 # Protected class variables.
97 98 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
98 99 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
99 100 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
100 101 _input_splitter_class = InputSplitter
101 102
102 103 #---------------------------------------------------------------------------
103 104 # 'object' interface
104 105 #---------------------------------------------------------------------------
105 106
106 107 def __init__(self, *args, **kw):
107 108 super(FrontendWidget, self).__init__(*args, **kw)
108 109
109 110 # FrontendWidget protected variables.
110 111 self._bracket_matcher = BracketMatcher(self._control)
111 112 self._call_tip_widget = CallTipWidget(self._control)
112 113 self._completion_lexer = CompletionLexer(PythonLexer())
113 114 self._hidden = False
114 115 self._highlighter = FrontendHighlighter(self)
115 116 self._input_splitter = self._input_splitter_class(input_mode='block')
116 117 self._kernel_manager = None
117 118 self._possible_kernel_restart = False
118 119 self._request_info = {}
119 120
120 121 # Configure the ConsoleWidget.
121 122 self.tab_width = 4
122 123 self._set_continuation_prompt('... ')
123 124
124 125 # Connect signal handlers.
125 126 document = self._control.document()
126 127 document.contentsChange.connect(self._document_contents_change)
127 128
128 129 #---------------------------------------------------------------------------
129 130 # 'ConsoleWidget' abstract interface
130 131 #---------------------------------------------------------------------------
131 132
132 133 def _is_complete(self, source, interactive):
133 134 """ Returns whether 'source' can be completely processed and a new
134 135 prompt created. When triggered by an Enter/Return key press,
135 136 'interactive' is True; otherwise, it is False.
136 137 """
137 138 complete = self._input_splitter.push(source.expandtabs(4))
138 139 if interactive:
139 140 complete = not self._input_splitter.push_accepts_more()
140 141 return complete
141 142
142 143 def _execute(self, source, hidden):
143 144 """ Execute 'source'. If 'hidden', do not show any output.
144 145
145 146 See parent class :meth:`execute` docstring for full details.
146 147 """
147 148 msg_id = self.kernel_manager.xreq_channel.execute(source, hidden)
148 149 self._request_info['execute'] = self._ExecutionRequest(msg_id, 'user')
149 150 self._hidden = hidden
150 151
151 152 def _prompt_started_hook(self):
152 153 """ Called immediately after a new prompt is displayed.
153 154 """
154 155 if not self._reading:
155 156 self._highlighter.highlighting_on = True
156 157
157 158 def _prompt_finished_hook(self):
158 159 """ Called immediately after a prompt is finished, i.e. when some input
159 160 will be processed and a new prompt displayed.
160 161 """
161 162 if not self._reading:
162 163 self._highlighter.highlighting_on = False
163 164
164 165 def _tab_pressed(self):
165 166 """ Called when the tab key is pressed. Returns whether to continue
166 167 processing the event.
167 168 """
168 169 # Perform tab completion if:
169 170 # 1) The cursor is in the input buffer.
170 171 # 2) There is a non-whitespace character before the cursor.
171 172 text = self._get_input_buffer_cursor_line()
172 173 if text is None:
173 174 return False
174 175 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
175 176 if complete:
176 177 self._complete()
177 178 return not complete
178 179
179 180 #---------------------------------------------------------------------------
180 181 # 'ConsoleWidget' protected interface
181 182 #---------------------------------------------------------------------------
182 183
183 184 def _event_filter_console_keypress(self, event):
184 185 """ Reimplemented to allow execution interruption.
185 186 """
186 187 key = event.key()
187 188 if self._control_key_down(event.modifiers(), include_command=False):
188 189 if key == QtCore.Qt.Key_C and self._executing:
189 190 self.interrupt_kernel()
190 191 return True
191 192 elif key == QtCore.Qt.Key_Period:
192 193 message = 'Are you sure you want to restart the kernel?'
193 194 self.restart_kernel(message)
194 195 return True
195 196 return super(FrontendWidget, self)._event_filter_console_keypress(event)
196 197
197 198 def _insert_continuation_prompt(self, cursor):
198 199 """ Reimplemented for auto-indentation.
199 200 """
200 201 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
201 202 spaces = self._input_splitter.indent_spaces
202 203 cursor.insertText('\t' * (spaces / self.tab_width))
203 204 cursor.insertText(' ' * (spaces % self.tab_width))
204 205
205 206 #---------------------------------------------------------------------------
206 207 # 'BaseFrontendMixin' abstract interface
207 208 #---------------------------------------------------------------------------
208 209
209 210 def _handle_complete_reply(self, rep):
210 211 """ Handle replies for tab completion.
211 212 """
212 213 cursor = self._get_cursor()
213 214 info = self._request_info.get('complete')
214 215 if info and info.id == rep['parent_header']['msg_id'] and \
215 216 info.pos == cursor.position():
216 217 text = '.'.join(self._get_context())
217 218 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
218 219 self._complete_with_items(cursor, rep['content']['matches'])
219 220
220 221 def _handle_execute_reply(self, msg):
221 222 """ Handles replies for code execution.
222 223 """
223 224 info = self._request_info.get('execute')
224 225 if info and info.id == msg['parent_header']['msg_id'] and \
225 226 info.kind == 'user' and not self._hidden:
226 227 # Make sure that all output from the SUB channel has been processed
227 228 # before writing a new prompt.
228 229 self.kernel_manager.sub_channel.flush()
229 230
230 231 content = msg['content']
231 232 status = content['status']
232 233 if status == 'ok':
233 234 self._process_execute_ok(msg)
234 235 elif status == 'error':
235 236 self._process_execute_error(msg)
236 237 elif status == 'abort':
237 238 self._process_execute_abort(msg)
238 239
239 240 self._show_interpreter_prompt_for_reply(msg)
240 241 self.executed.emit(msg)
241 242
242 243 def _handle_input_request(self, msg):
243 244 """ Handle requests for raw_input.
244 245 """
245 246 if self._hidden:
246 247 raise RuntimeError('Request for raw input during hidden execution.')
247 248
248 249 # Make sure that all output from the SUB channel has been processed
249 250 # before entering readline mode.
250 251 self.kernel_manager.sub_channel.flush()
251 252
252 253 def callback(line):
253 254 self.kernel_manager.rep_channel.input(line)
254 255 self._readline(msg['content']['prompt'], callback=callback)
255 256
256 257 def _handle_kernel_died(self, since_last_heartbeat):
257 258 """ Handle the kernel's death by asking if the user wants to restart.
258 259 """
259 260 message = 'The kernel heartbeat has been inactive for %.2f ' \
260 261 'seconds. Do you want to restart the kernel? You may ' \
261 262 'first want to check the network connection.' % \
262 263 since_last_heartbeat
263 264 if self.custom_restart:
264 265 self.custom_restart_kernel_died.emit(since_last_heartbeat)
265 266 else:
266 267 self.restart_kernel(message)
267 268
268 269 def _handle_object_info_reply(self, rep):
269 270 """ Handle replies for call tips.
270 271 """
271 272 cursor = self._get_cursor()
272 273 info = self._request_info.get('call_tip')
273 274 if info and info.id == rep['parent_header']['msg_id'] and \
274 275 info.pos == cursor.position():
275 276 doc = rep['content']['docstring']
276 277 if doc:
277 278 self._call_tip_widget.show_docstring(doc)
278 279
279 280 def _handle_pyout(self, msg):
280 281 """ Handle display hook output.
281 282 """
282 283 if not self._hidden and self._is_from_this_session(msg):
283 284 self._append_plain_text(msg['content']['data'] + '\n')
284 285
285 286 def _handle_stream(self, msg):
286 287 """ Handle stdout, stderr, and stdin.
287 288 """
288 289 if not self._hidden and self._is_from_this_session(msg):
289 290 self._append_plain_text(msg['content']['data'])
290 291 self._control.moveCursor(QtGui.QTextCursor.End)
291 292
292 293 def _started_channels(self):
293 294 """ Called when the KernelManager channels have started listening or
294 295 when the frontend is assigned an already listening KernelManager.
295 296 """
296 297 self._control.clear()
297 298 self._append_plain_text(self._get_banner())
298 299 self._show_interpreter_prompt()
299 300
300 301 def _stopped_channels(self):
301 302 """ Called when the KernelManager channels have stopped listening or
302 303 when a listening KernelManager is removed from the frontend.
303 304 """
304 305 self._executing = self._reading = False
305 306 self._highlighter.highlighting_on = False
306 307
307 308 #---------------------------------------------------------------------------
308 309 # 'FrontendWidget' interface
309 310 #---------------------------------------------------------------------------
310 311
311 312 def execute_file(self, path, hidden=False):
312 313 """ Attempts to execute file with 'path'. If 'hidden', no output is
313 314 shown.
314 315 """
315 316 self.execute('execfile("%s")' % path, hidden=hidden)
316 317
317 318 def interrupt_kernel(self):
318 319 """ Attempts to interrupt the running kernel.
319 320 """
320 321 if self.custom_interrupt:
321 322 self.custom_interrupt_requested.emit()
322 323 elif self.kernel_manager.has_kernel:
323 324 self.kernel_manager.signal_kernel(signal.SIGINT)
324 325 else:
325 326 self._append_plain_text('Kernel process is either remote or '
326 327 'unspecified. Cannot interrupt.\n')
327 328
328 329 def restart_kernel(self, message):
329 330 """ Attempts to restart the running kernel.
330 331 """
331 332 # We want to make sure that if this dialog is already happening, that
332 333 # other signals don't trigger it again. This can happen when the
333 334 # kernel_died heartbeat signal is emitted and the user is slow to
334 335 # respond to the dialog.
335 336 if not self._possible_kernel_restart:
336 337 if self.custom_restart:
337 338 self.custom_restart_requested.emit()
338 339 elif self.kernel_manager.has_kernel:
339 340 # Setting this to True will prevent this logic from happening
340 341 # again until the current pass is completed.
341 342 self._possible_kernel_restart = True
342 343 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
343 344 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
344 345 message, buttons)
345 346 if result == QtGui.QMessageBox.Yes:
346 347 try:
347 348 self.kernel_manager.restart_kernel()
348 349 except RuntimeError:
349 350 message = 'Kernel started externally. Cannot restart.\n'
350 351 self._append_plain_text(message)
351 352 else:
352 353 self._stopped_channels()
353 354 self._append_plain_text('Kernel restarting...\n')
354 355 self._show_interpreter_prompt()
355 356 # This might need to be moved to another location?
356 357 self._possible_kernel_restart = False
357 358 else:
358 359 self._append_plain_text('Kernel process is either remote or '
359 360 'unspecified. Cannot restart.\n')
360 361
361 362 #---------------------------------------------------------------------------
362 363 # 'FrontendWidget' protected interface
363 364 #---------------------------------------------------------------------------
364 365
365 366 def _call_tip(self):
366 367 """ Shows a call tip, if appropriate, at the current cursor location.
367 368 """
368 369 # Decide if it makes sense to show a call tip
369 370 cursor = self._get_cursor()
370 371 cursor.movePosition(QtGui.QTextCursor.Left)
371 372 if cursor.document().characterAt(cursor.position()).toAscii() != '(':
372 373 return False
373 374 context = self._get_context(cursor)
374 375 if not context:
375 376 return False
376 377
377 378 # Send the metadata request to the kernel
378 379 name = '.'.join(context)
379 380 msg_id = self.kernel_manager.xreq_channel.object_info(name)
380 381 pos = self._get_cursor().position()
381 382 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
382 383 return True
383 384
384 385 def _complete(self):
385 386 """ Performs completion at the current cursor location.
386 387 """
387 388 context = self._get_context()
388 389 if context:
389 390 # Send the completion request to the kernel
390 391 msg_id = self.kernel_manager.xreq_channel.complete(
391 392 '.'.join(context), # text
392 393 self._get_input_buffer_cursor_line(), # line
393 394 self._get_input_buffer_cursor_column(), # cursor_pos
394 395 self.input_buffer) # block
395 396 pos = self._get_cursor().position()
396 397 info = self._CompletionRequest(msg_id, pos)
397 398 self._request_info['complete'] = info
398 399
399 400 def _get_banner(self):
400 401 """ Gets a banner to display at the beginning of a session.
401 402 """
402 403 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
403 404 '"license" for more information.'
404 405 return banner % (sys.version, sys.platform)
405 406
406 407 def _get_context(self, cursor=None):
407 408 """ Gets the context for the specified cursor (or the current cursor
408 409 if none is specified).
409 410 """
410 411 if cursor is None:
411 412 cursor = self._get_cursor()
412 413 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
413 414 QtGui.QTextCursor.KeepAnchor)
414 415 text = str(cursor.selection().toPlainText())
415 416 return self._completion_lexer.get_context(text)
416 417
417 418 def _process_execute_abort(self, msg):
418 419 """ Process a reply for an aborted execution request.
419 420 """
420 421 self._append_plain_text("ERROR: execution aborted\n")
421 422
422 423 def _process_execute_error(self, msg):
423 424 """ Process a reply for an execution request that resulted in an error.
424 425 """
425 426 content = msg['content']
426 427 traceback = ''.join(content['traceback'])
427 428 self._append_plain_text(traceback)
428 429
429 430 def _process_execute_ok(self, msg):
430 431 """ Process a reply for a successful execution equest.
431 432 """
432 433 payload = msg['content']['payload']
433 434 for item in payload:
434 435 if not self._process_execute_payload(item):
435 warning = 'Received unknown payload of type %s\n'
436 self._append_plain_text(warning % repr(item['source']))
436 warning = 'Warning: received unknown payload of type %s'
437 raw_print(warning % repr(item['source']))
437 438
438 439 def _process_execute_payload(self, item):
439 440 """ Process a single payload item from the list of payload items in an
440 441 execution reply. Returns whether the payload was handled.
441 442 """
442 443 # The basic FrontendWidget doesn't handle payloads, as they are a
443 444 # mechanism for going beyond the standard Python interpreter model.
444 445 return False
445 446
446 447 def _show_interpreter_prompt(self):
447 448 """ Shows a prompt for the interpreter.
448 449 """
449 450 self._show_prompt('>>> ')
450 451
451 452 def _show_interpreter_prompt_for_reply(self, msg):
452 453 """ Shows a prompt for the interpreter given an 'execute_reply' message.
453 454 """
454 455 self._show_interpreter_prompt()
455 456
456 457 #------ Signal handlers ----------------------------------------------------
457 458
458 459 def _document_contents_change(self, position, removed, added):
459 460 """ Called whenever the document's content changes. Display a call tip
460 461 if appropriate.
461 462 """
462 463 # Calculate where the cursor should be *after* the change:
463 464 position += added
464 465
465 466 document = self._control.document()
466 467 if position == self._get_cursor().position():
467 468 self._call_tip()
General Comments 0
You need to be logged in to leave comments. Login now