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