##// END OF EJS Templates
First cut at safe appending in the Qt console.
epatters -
Show More
@@ -1,1797 +1,1807 b''
1 1 """ An abstract base class for console-type widgets.
2 2 """
3 3 #-----------------------------------------------------------------------------
4 4 # Imports
5 5 #-----------------------------------------------------------------------------
6 6
7 7 # Standard library imports
8 8 import os
9 9 from os.path import commonprefix
10 10 import re
11 11 import sys
12 12 from textwrap import dedent
13 13 from unicodedata import category
14 14
15 15 # System library imports
16 16 from IPython.external.qt import QtCore, QtGui
17 17
18 18 # Local imports
19 19 from IPython.config.configurable import Configurable
20 20 from IPython.frontend.qt.rich_text import HtmlExporter
21 21 from IPython.frontend.qt.util import MetaQObjectHasTraits, get_font
22 22 from IPython.utils.traitlets import Bool, Enum, Int, Unicode
23 23 from ansi_code_processor import QtAnsiCodeProcessor
24 24 from completion_widget import CompletionWidget
25 25 from kill_ring import QtKillRing
26 26
27 27 #-----------------------------------------------------------------------------
28 28 # Functions
29 29 #-----------------------------------------------------------------------------
30 30
31 31 def is_letter_or_number(char):
32 32 """ Returns whether the specified unicode character is a letter or a number.
33 33 """
34 34 cat = category(char)
35 35 return cat.startswith('L') or cat.startswith('N')
36 36
37 37 #-----------------------------------------------------------------------------
38 38 # Classes
39 39 #-----------------------------------------------------------------------------
40 40
41 41 class ConsoleWidget(Configurable, QtGui.QWidget):
42 42 """ An abstract base class for console-type widgets. This class has
43 43 functionality for:
44 44
45 45 * Maintaining a prompt and editing region
46 46 * Providing the traditional Unix-style console keyboard shortcuts
47 47 * Performing tab completion
48 48 * Paging text
49 49 * Handling ANSI escape codes
50 50
51 51 ConsoleWidget also provides a number of utility methods that will be
52 52 convenient to implementors of a console-style widget.
53 53 """
54 54 __metaclass__ = MetaQObjectHasTraits
55 55
56 56 #------ Configuration ------------------------------------------------------
57 57
58 58 ansi_codes = Bool(True, config=True,
59 59 help="Whether to process ANSI escape codes."
60 60 )
61 61 buffer_size = Int(500, config=True,
62 62 help="""
63 63 The maximum number of lines of text before truncation. Specifying a
64 64 non-positive number disables text truncation (not recommended).
65 65 """
66 66 )
67 67 gui_completion = Bool(False, config=True,
68 68 help="Use a list widget instead of plain text output for tab completion."
69 69 )
70 70 # NOTE: this value can only be specified during initialization.
71 71 kind = Enum(['plain', 'rich'], default_value='plain', config=True,
72 72 help="""
73 73 The type of underlying text widget to use. Valid values are 'plain', which
74 74 specifies a QPlainTextEdit, and 'rich', which specifies a QTextEdit.
75 75 """
76 76 )
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 help="""
81 81 The type of paging to use. Valid values are:
82 82
83 83 'inside' : The widget pages like a traditional terminal.
84 84 'hsplit' : When paging is requested, the widget is split
85 85 horizontally. The top pane contains the console, and the
86 86 bottom pane contains the paged text.
87 87 'vsplit' : Similar to 'hsplit', except that a vertical splitter used.
88 88 'custom' : No action is taken by the widget beyond emitting a
89 89 'custom_page_requested(str)' signal.
90 90 'none' : The text is written directly to the console.
91 91 """)
92 92
93 93 font_family = Unicode(config=True,
94 94 help="""The font family to use for the console.
95 95 On OSX this defaults to Monaco, on Windows the default is
96 96 Consolas with fallback of Courier, and on other platforms
97 97 the default is Monospace.
98 98 """)
99 99 def _font_family_default(self):
100 100 if sys.platform == 'win32':
101 101 # Consolas ships with Vista/Win7, fallback to Courier if needed
102 102 return 'Consolas'
103 103 elif sys.platform == 'darwin':
104 104 # OSX always has Monaco, no need for a fallback
105 105 return 'Monaco'
106 106 else:
107 107 # Monospace should always exist, no need for a fallback
108 108 return 'Monospace'
109 109
110 110 font_size = Int(config=True,
111 111 help="""The font size. If unconfigured, Qt will be entrusted
112 112 with the size of the font.
113 113 """)
114 114
115 115 # Whether to override ShortcutEvents for the keybindings defined by this
116 116 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
117 117 # priority (when it has focus) over, e.g., window-level menu shortcuts.
118 118 override_shortcuts = Bool(False)
119 119
120 120 #------ Signals ------------------------------------------------------------
121 121
122 122 # Signals that indicate ConsoleWidget state.
123 123 copy_available = QtCore.Signal(bool)
124 124 redo_available = QtCore.Signal(bool)
125 125 undo_available = QtCore.Signal(bool)
126 126
127 127 # Signal emitted when paging is needed and the paging style has been
128 128 # specified as 'custom'.
129 129 custom_page_requested = QtCore.Signal(object)
130 130
131 131 # Signal emitted when the font is changed.
132 132 font_changed = QtCore.Signal(QtGui.QFont)
133 133
134 134 #------ Protected class variables ------------------------------------------
135 135
136 136 # When the control key is down, these keys are mapped.
137 137 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
138 138 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
139 139 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
140 140 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
141 141 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
142 142 QtCore.Qt.Key_H : QtCore.Qt.Key_Backspace,
143 143 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
144 144 if not sys.platform == 'darwin':
145 145 # On OS X, Ctrl-E already does the right thing, whereas End moves the
146 146 # cursor to the bottom of the buffer.
147 147 _ctrl_down_remap[QtCore.Qt.Key_E] = QtCore.Qt.Key_End
148 148
149 149 # The shortcuts defined by this widget. We need to keep track of these to
150 150 # support 'override_shortcuts' above.
151 151 _shortcuts = set(_ctrl_down_remap.keys() +
152 152 [ QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
153 153 QtCore.Qt.Key_V ])
154 154
155 155 #---------------------------------------------------------------------------
156 156 # 'QObject' interface
157 157 #---------------------------------------------------------------------------
158 158
159 159 def __init__(self, parent=None, **kw):
160 160 """ Create a ConsoleWidget.
161 161
162 162 Parameters:
163 163 -----------
164 164 parent : QWidget, optional [default None]
165 165 The parent for this widget.
166 166 """
167 167 QtGui.QWidget.__init__(self, parent)
168 168 Configurable.__init__(self, **kw)
169 169
170 170 # Create the layout and underlying text widget.
171 171 layout = QtGui.QStackedLayout(self)
172 172 layout.setContentsMargins(0, 0, 0, 0)
173 173 self._control = self._create_control()
174 174 self._page_control = None
175 175 self._splitter = None
176 176 if self.paging in ('hsplit', 'vsplit'):
177 177 self._splitter = QtGui.QSplitter()
178 178 if self.paging == 'hsplit':
179 179 self._splitter.setOrientation(QtCore.Qt.Horizontal)
180 180 else:
181 181 self._splitter.setOrientation(QtCore.Qt.Vertical)
182 182 self._splitter.addWidget(self._control)
183 183 layout.addWidget(self._splitter)
184 184 else:
185 185 layout.addWidget(self._control)
186 186
187 187 # Create the paging widget, if necessary.
188 188 if self.paging in ('inside', 'hsplit', 'vsplit'):
189 189 self._page_control = self._create_page_control()
190 190 if self._splitter:
191 191 self._page_control.hide()
192 192 self._splitter.addWidget(self._page_control)
193 193 else:
194 194 layout.addWidget(self._page_control)
195 195
196 196 # Initialize protected variables. Some variables contain useful state
197 197 # information for subclasses; they should be considered read-only.
198 198 self._ansi_processor = QtAnsiCodeProcessor()
199 199 self._completion_widget = CompletionWidget(self._control)
200 200 self._continuation_prompt = '> '
201 201 self._continuation_prompt_html = None
202 202 self._executing = False
203 203 self._filter_drag = False
204 204 self._filter_resize = False
205 205 self._html_exporter = HtmlExporter(self._control)
206 206 self._input_buffer_executing = ''
207 207 self._input_buffer_pending = ''
208 208 self._kill_ring = QtKillRing(self._control)
209 209 self._prompt = ''
210 210 self._prompt_html = None
211 211 self._prompt_pos = 0
212 212 self._prompt_sep = ''
213 213 self._reading = False
214 214 self._reading_callback = None
215 215 self._tab_width = 8
216 216 self._text_completing_pos = 0
217 217
218 218 # Set a monospaced font.
219 219 self.reset_font()
220 220
221 221 # Configure actions.
222 222 action = QtGui.QAction('Print', None)
223 223 action.setEnabled(True)
224 224 printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
225 225 if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
226 226 # Only override the default if there is a collision.
227 227 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
228 228 printkey = "Ctrl+Shift+P"
229 229 action.setShortcut(printkey)
230 230 action.triggered.connect(self.print_)
231 231 self.addAction(action)
232 232 self._print_action = action
233 233
234 234 action = QtGui.QAction('Save as HTML/XML', None)
235 235 action.setShortcut(QtGui.QKeySequence.Save)
236 236 action.triggered.connect(self.export_html)
237 237 self.addAction(action)
238 238 self._export_action = action
239 239
240 240 action = QtGui.QAction('Select All', None)
241 241 action.setEnabled(True)
242 242 action.setShortcut(QtGui.QKeySequence.SelectAll)
243 243 action.triggered.connect(self.select_all)
244 244 self.addAction(action)
245 245 self._select_all_action = action
246 246
247 247 def eventFilter(self, obj, event):
248 248 """ Reimplemented to ensure a console-like behavior in the underlying
249 249 text widgets.
250 250 """
251 251 etype = event.type()
252 252 if etype == QtCore.QEvent.KeyPress:
253 253
254 254 # Re-map keys for all filtered widgets.
255 255 key = event.key()
256 256 if self._control_key_down(event.modifiers()) and \
257 257 key in self._ctrl_down_remap:
258 258 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
259 259 self._ctrl_down_remap[key],
260 260 QtCore.Qt.NoModifier)
261 261 QtGui.qApp.sendEvent(obj, new_event)
262 262 return True
263 263
264 264 elif obj == self._control:
265 265 return self._event_filter_console_keypress(event)
266 266
267 267 elif obj == self._page_control:
268 268 return self._event_filter_page_keypress(event)
269 269
270 270 # Make middle-click paste safe.
271 271 elif etype == QtCore.QEvent.MouseButtonRelease and \
272 272 event.button() == QtCore.Qt.MidButton and \
273 273 obj == self._control.viewport():
274 274 cursor = self._control.cursorForPosition(event.pos())
275 275 self._control.setTextCursor(cursor)
276 276 self.paste(QtGui.QClipboard.Selection)
277 277 return True
278 278
279 279 # Manually adjust the scrollbars *after* a resize event is dispatched.
280 280 elif etype == QtCore.QEvent.Resize and not self._filter_resize:
281 281 self._filter_resize = True
282 282 QtGui.qApp.sendEvent(obj, event)
283 283 self._adjust_scrollbars()
284 284 self._filter_resize = False
285 285 return True
286 286
287 287 # Override shortcuts for all filtered widgets.
288 288 elif etype == QtCore.QEvent.ShortcutOverride and \
289 289 self.override_shortcuts and \
290 290 self._control_key_down(event.modifiers()) and \
291 291 event.key() in self._shortcuts:
292 292 event.accept()
293 293
294 294 # Ensure that drags are safe. The problem is that the drag starting
295 295 # logic, which determines whether the drag is a Copy or Move, is locked
296 296 # down in QTextControl. If the widget is editable, which it must be if
297 297 # we're not executing, the drag will be a Move. The following hack
298 298 # prevents QTextControl from deleting the text by clearing the selection
299 299 # when a drag leave event originating from this widget is dispatched.
300 300 # The fact that we have to clear the user's selection is unfortunate,
301 301 # but the alternative--trying to prevent Qt from using its hardwired
302 302 # drag logic and writing our own--is worse.
303 303 elif etype == QtCore.QEvent.DragEnter and \
304 304 obj == self._control.viewport() and \
305 305 event.source() == self._control.viewport():
306 306 self._filter_drag = True
307 307 elif etype == QtCore.QEvent.DragLeave and \
308 308 obj == self._control.viewport() and \
309 309 self._filter_drag:
310 310 cursor = self._control.textCursor()
311 311 cursor.clearSelection()
312 312 self._control.setTextCursor(cursor)
313 313 self._filter_drag = False
314 314
315 315 # Ensure that drops are safe.
316 316 elif etype == QtCore.QEvent.Drop and obj == self._control.viewport():
317 317 cursor = self._control.cursorForPosition(event.pos())
318 318 if self._in_buffer(cursor.position()):
319 319 text = event.mimeData().text()
320 320 self._insert_plain_text_into_buffer(cursor, text)
321 321
322 322 # Qt is expecting to get something here--drag and drop occurs in its
323 323 # own event loop. Send a DragLeave event to end it.
324 324 QtGui.qApp.sendEvent(obj, QtGui.QDragLeaveEvent())
325 325 return True
326 326
327 327 return super(ConsoleWidget, self).eventFilter(obj, event)
328 328
329 329 #---------------------------------------------------------------------------
330 330 # 'QWidget' interface
331 331 #---------------------------------------------------------------------------
332 332
333 333 def sizeHint(self):
334 334 """ Reimplemented to suggest a size that is 80 characters wide and
335 335 25 lines high.
336 336 """
337 337 font_metrics = QtGui.QFontMetrics(self.font)
338 338 margin = (self._control.frameWidth() +
339 339 self._control.document().documentMargin()) * 2
340 340 style = self.style()
341 341 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
342 342
343 343 # Note 1: Despite my best efforts to take the various margins into
344 344 # account, the width is still coming out a bit too small, so we include
345 345 # a fudge factor of one character here.
346 346 # Note 2: QFontMetrics.maxWidth is not used here or anywhere else due
347 347 # to a Qt bug on certain Mac OS systems where it returns 0.
348 348 width = font_metrics.width(' ') * 81 + margin
349 349 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
350 350 if self.paging == 'hsplit':
351 351 width = width * 2 + splitwidth
352 352
353 353 height = font_metrics.height() * 25 + margin
354 354 if self.paging == 'vsplit':
355 355 height = height * 2 + splitwidth
356 356
357 357 return QtCore.QSize(width, height)
358 358
359 359 #---------------------------------------------------------------------------
360 360 # 'ConsoleWidget' public interface
361 361 #---------------------------------------------------------------------------
362 362
363 363 def can_copy(self):
364 364 """ Returns whether text can be copied to the clipboard.
365 365 """
366 366 return self._control.textCursor().hasSelection()
367 367
368 368 def can_cut(self):
369 369 """ Returns whether text can be cut to the clipboard.
370 370 """
371 371 cursor = self._control.textCursor()
372 372 return (cursor.hasSelection() and
373 373 self._in_buffer(cursor.anchor()) and
374 374 self._in_buffer(cursor.position()))
375 375
376 376 def can_paste(self):
377 377 """ Returns whether text can be pasted from the clipboard.
378 378 """
379 379 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
380 380 return bool(QtGui.QApplication.clipboard().text())
381 381 return False
382 382
383 383 def clear(self, keep_input=True):
384 384 """ Clear the console.
385 385
386 386 Parameters:
387 387 -----------
388 388 keep_input : bool, optional (default True)
389 389 If set, restores the old input buffer if a new prompt is written.
390 390 """
391 391 if self._executing:
392 392 self._control.clear()
393 393 else:
394 394 if keep_input:
395 395 input_buffer = self.input_buffer
396 396 self._control.clear()
397 397 self._show_prompt()
398 398 if keep_input:
399 399 self.input_buffer = input_buffer
400 400
401 401 def copy(self):
402 402 """ Copy the currently selected text to the clipboard.
403 403 """
404 404 self._control.copy()
405 405
406 406 def cut(self):
407 407 """ Copy the currently selected text to the clipboard and delete it
408 408 if it's inside the input buffer.
409 409 """
410 410 self.copy()
411 411 if self.can_cut():
412 412 self._control.textCursor().removeSelectedText()
413 413
414 414 def execute(self, source=None, hidden=False, interactive=False):
415 415 """ Executes source or the input buffer, possibly prompting for more
416 416 input.
417 417
418 418 Parameters:
419 419 -----------
420 420 source : str, optional
421 421
422 422 The source to execute. If not specified, the input buffer will be
423 423 used. If specified and 'hidden' is False, the input buffer will be
424 424 replaced with the source before execution.
425 425
426 426 hidden : bool, optional (default False)
427 427
428 428 If set, no output will be shown and the prompt will not be modified.
429 429 In other words, it will be completely invisible to the user that
430 430 an execution has occurred.
431 431
432 432 interactive : bool, optional (default False)
433 433
434 434 Whether the console is to treat the source as having been manually
435 435 entered by the user. The effect of this parameter depends on the
436 436 subclass implementation.
437 437
438 438 Raises:
439 439 -------
440 440 RuntimeError
441 441 If incomplete input is given and 'hidden' is True. In this case,
442 442 it is not possible to prompt for more input.
443 443
444 444 Returns:
445 445 --------
446 446 A boolean indicating whether the source was executed.
447 447 """
448 448 # WARNING: The order in which things happen here is very particular, in
449 449 # large part because our syntax highlighting is fragile. If you change
450 450 # something, test carefully!
451 451
452 452 # Decide what to execute.
453 453 if source is None:
454 454 source = self.input_buffer
455 455 if not hidden:
456 456 # A newline is appended later, but it should be considered part
457 457 # of the input buffer.
458 458 source += '\n'
459 459 elif not hidden:
460 460 self.input_buffer = source
461 461
462 462 # Execute the source or show a continuation prompt if it is incomplete.
463 463 complete = self._is_complete(source, interactive)
464 464 if hidden:
465 465 if complete:
466 466 self._execute(source, hidden)
467 467 else:
468 468 error = 'Incomplete noninteractive input: "%s"'
469 469 raise RuntimeError(error % source)
470 470 else:
471 471 if complete:
472 472 self._append_plain_text('\n')
473 473 self._input_buffer_executing = self.input_buffer
474 474 self._executing = True
475 475 self._prompt_finished()
476 476
477 477 # The maximum block count is only in effect during execution.
478 478 # This ensures that _prompt_pos does not become invalid due to
479 479 # text truncation.
480 480 self._control.document().setMaximumBlockCount(self.buffer_size)
481 481
482 482 # Setting a positive maximum block count will automatically
483 483 # disable the undo/redo history, but just to be safe:
484 484 self._control.setUndoRedoEnabled(False)
485 485
486 486 # Perform actual execution.
487 487 self._execute(source, hidden)
488 488
489 489 else:
490 490 # Do this inside an edit block so continuation prompts are
491 491 # removed seamlessly via undo/redo.
492 492 cursor = self._get_end_cursor()
493 493 cursor.beginEditBlock()
494 494 cursor.insertText('\n')
495 495 self._insert_continuation_prompt(cursor)
496 496 cursor.endEditBlock()
497 497
498 498 # Do not do this inside the edit block. It works as expected
499 499 # when using a QPlainTextEdit control, but does not have an
500 500 # effect when using a QTextEdit. I believe this is a Qt bug.
501 501 self._control.moveCursor(QtGui.QTextCursor.End)
502 502
503 503 return complete
504 504
505 505 def export_html(self):
506 506 """ Shows a dialog to export HTML/XML in various formats.
507 507 """
508 508 self._html_exporter.export()
509 509
510 510 def _get_input_buffer(self):
511 511 """ The text that the user has entered entered at the current prompt.
512 512
513 513 If the console is currently executing, the text that is executing will
514 514 always be returned.
515 515 """
516 516 # If we're executing, the input buffer may not even exist anymore due to
517 517 # the limit imposed by 'buffer_size'. Therefore, we store it.
518 518 if self._executing:
519 519 return self._input_buffer_executing
520 520
521 521 cursor = self._get_end_cursor()
522 522 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
523 523 input_buffer = cursor.selection().toPlainText()
524 524
525 525 # Strip out continuation prompts.
526 526 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
527 527
528 528 def _set_input_buffer(self, string):
529 529 """ Sets the text in the input buffer.
530 530
531 531 If the console is currently executing, this call has no *immediate*
532 532 effect. When the execution is finished, the input buffer will be updated
533 533 appropriately.
534 534 """
535 535 # If we're executing, store the text for later.
536 536 if self._executing:
537 537 self._input_buffer_pending = string
538 538 return
539 539
540 540 # Remove old text.
541 541 cursor = self._get_end_cursor()
542 542 cursor.beginEditBlock()
543 543 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
544 544 cursor.removeSelectedText()
545 545
546 546 # Insert new text with continuation prompts.
547 547 self._insert_plain_text_into_buffer(self._get_prompt_cursor(), string)
548 548 cursor.endEditBlock()
549 549 self._control.moveCursor(QtGui.QTextCursor.End)
550 550
551 551 input_buffer = property(_get_input_buffer, _set_input_buffer)
552 552
553 553 def _get_font(self):
554 554 """ The base font being used by the ConsoleWidget.
555 555 """
556 556 return self._control.document().defaultFont()
557 557
558 558 def _set_font(self, font):
559 559 """ Sets the base font for the ConsoleWidget to the specified QFont.
560 560 """
561 561 font_metrics = QtGui.QFontMetrics(font)
562 562 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
563 563
564 564 self._completion_widget.setFont(font)
565 565 self._control.document().setDefaultFont(font)
566 566 if self._page_control:
567 567 self._page_control.document().setDefaultFont(font)
568 568
569 569 self.font_changed.emit(font)
570 570
571 571 font = property(_get_font, _set_font)
572 572
573 573 def paste(self, mode=QtGui.QClipboard.Clipboard):
574 574 """ Paste the contents of the clipboard into the input region.
575 575
576 576 Parameters:
577 577 -----------
578 578 mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
579 579
580 580 Controls which part of the system clipboard is used. This can be
581 581 used to access the selection clipboard in X11 and the Find buffer
582 582 in Mac OS. By default, the regular clipboard is used.
583 583 """
584 584 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
585 585 # Make sure the paste is safe.
586 586 self._keep_cursor_in_buffer()
587 587 cursor = self._control.textCursor()
588 588
589 589 # Remove any trailing newline, which confuses the GUI and forces the
590 590 # user to backspace.
591 591 text = QtGui.QApplication.clipboard().text(mode).rstrip()
592 592 self._insert_plain_text_into_buffer(cursor, dedent(text))
593 593
594 594 def print_(self, printer = None):
595 595 """ Print the contents of the ConsoleWidget to the specified QPrinter.
596 596 """
597 597 if (not printer):
598 598 printer = QtGui.QPrinter()
599 599 if(QtGui.QPrintDialog(printer).exec_() != QtGui.QDialog.Accepted):
600 600 return
601 601 self._control.print_(printer)
602 602
603 603 def prompt_to_top(self):
604 604 """ Moves the prompt to the top of the viewport.
605 605 """
606 606 if not self._executing:
607 607 prompt_cursor = self._get_prompt_cursor()
608 608 if self._get_cursor().blockNumber() < prompt_cursor.blockNumber():
609 609 self._set_cursor(prompt_cursor)
610 610 self._set_top_cursor(prompt_cursor)
611 611
612 612 def redo(self):
613 613 """ Redo the last operation. If there is no operation to redo, nothing
614 614 happens.
615 615 """
616 616 self._control.redo()
617 617
618 618 def reset_font(self):
619 619 """ Sets the font to the default fixed-width font for this platform.
620 620 """
621 621 if sys.platform == 'win32':
622 622 # Consolas ships with Vista/Win7, fallback to Courier if needed
623 623 fallback = 'Courier'
624 624 elif sys.platform == 'darwin':
625 625 # OSX always has Monaco
626 626 fallback = 'Monaco'
627 627 else:
628 628 # Monospace should always exist
629 629 fallback = 'Monospace'
630 630 font = get_font(self.font_family, fallback)
631 631 if self.font_size:
632 632 font.setPointSize(self.font_size)
633 633 else:
634 634 font.setPointSize(QtGui.qApp.font().pointSize())
635 635 font.setStyleHint(QtGui.QFont.TypeWriter)
636 636 self._set_font(font)
637 637
638 638 def change_font_size(self, delta):
639 639 """Change the font size by the specified amount (in points).
640 640 """
641 641 font = self.font
642 642 size = max(font.pointSize() + delta, 1) # minimum 1 point
643 643 font.setPointSize(size)
644 644 self._set_font(font)
645 645
646 646 def select_all(self):
647 647 """ Selects all the text in the buffer.
648 648 """
649 649 self._control.selectAll()
650 650
651 651 def _get_tab_width(self):
652 652 """ The width (in terms of space characters) for tab characters.
653 653 """
654 654 return self._tab_width
655 655
656 656 def _set_tab_width(self, tab_width):
657 657 """ Sets the width (in terms of space characters) for tab characters.
658 658 """
659 659 font_metrics = QtGui.QFontMetrics(self.font)
660 660 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
661 661
662 662 self._tab_width = tab_width
663 663
664 664 tab_width = property(_get_tab_width, _set_tab_width)
665 665
666 666 def undo(self):
667 667 """ Undo the last operation. If there is no operation to undo, nothing
668 668 happens.
669 669 """
670 670 self._control.undo()
671 671
672 672 #---------------------------------------------------------------------------
673 673 # 'ConsoleWidget' abstract interface
674 674 #---------------------------------------------------------------------------
675 675
676 676 def _is_complete(self, source, interactive):
677 677 """ Returns whether 'source' can be executed. When triggered by an
678 678 Enter/Return key press, 'interactive' is True; otherwise, it is
679 679 False.
680 680 """
681 681 raise NotImplementedError
682 682
683 683 def _execute(self, source, hidden):
684 684 """ Execute 'source'. If 'hidden', do not show any output.
685 685 """
686 686 raise NotImplementedError
687 687
688 688 def _prompt_started_hook(self):
689 689 """ Called immediately after a new prompt is displayed.
690 690 """
691 691 pass
692 692
693 693 def _prompt_finished_hook(self):
694 694 """ Called immediately after a prompt is finished, i.e. when some input
695 695 will be processed and a new prompt displayed.
696 696 """
697 697 pass
698 698
699 699 def _up_pressed(self, shift_modifier):
700 700 """ Called when the up key is pressed. Returns whether to continue
701 701 processing the event.
702 702 """
703 703 return True
704 704
705 705 def _down_pressed(self, shift_modifier):
706 706 """ Called when the down key is pressed. Returns whether to continue
707 707 processing the event.
708 708 """
709 709 return True
710 710
711 711 def _tab_pressed(self):
712 712 """ Called when the tab key is pressed. Returns whether to continue
713 713 processing the event.
714 714 """
715 715 return False
716 716
717 717 #--------------------------------------------------------------------------
718 718 # 'ConsoleWidget' protected interface
719 719 #--------------------------------------------------------------------------
720 720
721 def _append_html(self, html):
722 """ Appends html at the end of the console buffer.
721 def _append_custom(self, insert, input, before_prompt=False):
722 """ A low-level method for appending content to the end of the buffer.
723
724 If 'before_prompt' is enabled, the content will be inserted before the
725 current prompt, if there is one.
723 726 """
724 cursor = self._get_end_cursor()
725 self._insert_html(cursor, html)
727 # Determine where to insert the content.
728 cursor = self._control.textCursor()
729 if before_prompt and not self._executing:
730 cursor.setPosition(self._prompt_pos)
731 cursor.movePosition(QtGui.QTextCursor.Left, n=len(self._prompt))
732 else:
733 cursor.movePosition(QtGui.QTextCursor.End)
734 start_pos = cursor.position()
726 735
727 def _append_html_fetching_plain_text(self, html):
728 """ Appends 'html', then returns the plain text version of it.
729 """
730 cursor = self._get_end_cursor()
731 return self._insert_html_fetching_plain_text(cursor, html)
736 # Perform the insertion.
737 result = insert(cursor, input)
738
739 # Adjust the prompt position if we have inserted before it. This is safe
740 # because buffer truncation is disabled when not executing.
741 if before_prompt and not self._executing:
742 self._prompt_pos += cursor.position() - start_pos
743
744 return result
732 745
733 def _append_plain_text(self, text):
734 """ Appends plain text at the end of the console buffer, processing
735 ANSI codes if enabled.
746 def _append_html(self, html, before_prompt=False):
747 """ Appends HTML at the end of the console buffer.
736 748 """
737 cursor = self._get_end_cursor()
738 self._insert_plain_text(cursor, text)
749 self._append_custom(self._insert_html, html, before_prompt)
739 750
740 def _append_plain_text_keeping_prompt(self, text):
741 """ Writes 'text' after the current prompt, then restores the old prompt
742 with its old input buffer.
751 def _append_html_fetching_plain_text(self, html, before_prompt=False):
752 """ Appends HTML, then returns the plain text version of it.
743 753 """
744 input_buffer = self.input_buffer
745 self._append_plain_text('\n')
746 self._prompt_finished()
754 return self._append_custom(self._insert_html_fetching_plain_text,
755 html, before_prompt)
747 756
748 self._append_plain_text(text)
749 self._show_prompt()
750 self.input_buffer = input_buffer
757 def _append_plain_text(self, text, before_prompt=False):
758 """ Appends plain text, processing ANSI codes if enabled.
759 """
760 self._append_custom(self._insert_plain_text, text, before_prompt)
751 761
752 762 def _cancel_text_completion(self):
753 763 """ If text completion is progress, cancel it.
754 764 """
755 765 if self._text_completing_pos:
756 766 self._clear_temporary_buffer()
757 767 self._text_completing_pos = 0
758 768
759 769 def _clear_temporary_buffer(self):
760 770 """ Clears the "temporary text" buffer, i.e. all the text following
761 771 the prompt region.
762 772 """
763 773 # Select and remove all text below the input buffer.
764 774 cursor = self._get_prompt_cursor()
765 775 prompt = self._continuation_prompt.lstrip()
766 776 while cursor.movePosition(QtGui.QTextCursor.NextBlock):
767 777 temp_cursor = QtGui.QTextCursor(cursor)
768 778 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
769 779 text = temp_cursor.selection().toPlainText().lstrip()
770 780 if not text.startswith(prompt):
771 781 break
772 782 else:
773 783 # We've reached the end of the input buffer and no text follows.
774 784 return
775 785 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
776 786 cursor.movePosition(QtGui.QTextCursor.End,
777 787 QtGui.QTextCursor.KeepAnchor)
778 788 cursor.removeSelectedText()
779 789
780 790 # After doing this, we have no choice but to clear the undo/redo
781 791 # history. Otherwise, the text is not "temporary" at all, because it
782 792 # can be recalled with undo/redo. Unfortunately, Qt does not expose
783 793 # fine-grained control to the undo/redo system.
784 794 if self._control.isUndoRedoEnabled():
785 795 self._control.setUndoRedoEnabled(False)
786 796 self._control.setUndoRedoEnabled(True)
787 797
788 798 def _complete_with_items(self, cursor, items):
789 799 """ Performs completion with 'items' at the specified cursor location.
790 800 """
791 801 self._cancel_text_completion()
792 802
793 803 if len(items) == 1:
794 804 cursor.setPosition(self._control.textCursor().position(),
795 805 QtGui.QTextCursor.KeepAnchor)
796 806 cursor.insertText(items[0])
797 807
798 808 elif len(items) > 1:
799 809 current_pos = self._control.textCursor().position()
800 810 prefix = commonprefix(items)
801 811 if prefix:
802 812 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
803 813 cursor.insertText(prefix)
804 814 current_pos = cursor.position()
805 815
806 816 if self.gui_completion:
807 817 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
808 818 self._completion_widget.show_items(cursor, items)
809 819 else:
810 820 cursor.beginEditBlock()
811 821 self._append_plain_text('\n')
812 822 self._page(self._format_as_columns(items))
813 823 cursor.endEditBlock()
814 824
815 825 cursor.setPosition(current_pos)
816 826 self._control.moveCursor(QtGui.QTextCursor.End)
817 827 self._control.setTextCursor(cursor)
818 828 self._text_completing_pos = current_pos
819 829
820 830 def _context_menu_make(self, pos):
821 831 """ Creates a context menu for the given QPoint (in widget coordinates).
822 832 """
823 833 menu = QtGui.QMenu(self)
824 834
825 835 cut_action = menu.addAction('Cut', self.cut)
826 836 cut_action.setEnabled(self.can_cut())
827 837 cut_action.setShortcut(QtGui.QKeySequence.Cut)
828 838
829 839 copy_action = menu.addAction('Copy', self.copy)
830 840 copy_action.setEnabled(self.can_copy())
831 841 copy_action.setShortcut(QtGui.QKeySequence.Copy)
832 842
833 843 paste_action = menu.addAction('Paste', self.paste)
834 844 paste_action.setEnabled(self.can_paste())
835 845 paste_action.setShortcut(QtGui.QKeySequence.Paste)
836 846
837 847 menu.addSeparator()
838 848 menu.addAction(self._select_all_action)
839 849
840 850 menu.addSeparator()
841 851 menu.addAction(self._export_action)
842 852 menu.addAction(self._print_action)
843 853
844 854 return menu
845 855
846 856 def _control_key_down(self, modifiers, include_command=False):
847 857 """ Given a KeyboardModifiers flags object, return whether the Control
848 858 key is down.
849 859
850 860 Parameters:
851 861 -----------
852 862 include_command : bool, optional (default True)
853 863 Whether to treat the Command key as a (mutually exclusive) synonym
854 864 for Control when in Mac OS.
855 865 """
856 866 # Note that on Mac OS, ControlModifier corresponds to the Command key
857 867 # while MetaModifier corresponds to the Control key.
858 868 if sys.platform == 'darwin':
859 869 down = include_command and (modifiers & QtCore.Qt.ControlModifier)
860 870 return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
861 871 else:
862 872 return bool(modifiers & QtCore.Qt.ControlModifier)
863 873
864 874 def _create_control(self):
865 875 """ Creates and connects the underlying text widget.
866 876 """
867 877 # Create the underlying control.
868 878 if self.kind == 'plain':
869 879 control = QtGui.QPlainTextEdit()
870 880 elif self.kind == 'rich':
871 881 control = QtGui.QTextEdit()
872 882 control.setAcceptRichText(False)
873 883
874 884 # Install event filters. The filter on the viewport is needed for
875 885 # mouse events and drag events.
876 886 control.installEventFilter(self)
877 887 control.viewport().installEventFilter(self)
878 888
879 889 # Connect signals.
880 890 control.cursorPositionChanged.connect(self._cursor_position_changed)
881 891 control.customContextMenuRequested.connect(
882 892 self._custom_context_menu_requested)
883 893 control.copyAvailable.connect(self.copy_available)
884 894 control.redoAvailable.connect(self.redo_available)
885 895 control.undoAvailable.connect(self.undo_available)
886 896
887 897 # Hijack the document size change signal to prevent Qt from adjusting
888 898 # the viewport's scrollbar. We are relying on an implementation detail
889 899 # of Q(Plain)TextEdit here, which is potentially dangerous, but without
890 900 # this functionality we cannot create a nice terminal interface.
891 901 layout = control.document().documentLayout()
892 902 layout.documentSizeChanged.disconnect()
893 903 layout.documentSizeChanged.connect(self._adjust_scrollbars)
894 904
895 905 # Configure the control.
896 906 control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
897 907 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
898 908 control.setReadOnly(True)
899 909 control.setUndoRedoEnabled(False)
900 910 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
901 911 return control
902 912
903 913 def _create_page_control(self):
904 914 """ Creates and connects the underlying paging widget.
905 915 """
906 916 if self.kind == 'plain':
907 917 control = QtGui.QPlainTextEdit()
908 918 elif self.kind == 'rich':
909 919 control = QtGui.QTextEdit()
910 920 control.installEventFilter(self)
911 921 control.setReadOnly(True)
912 922 control.setUndoRedoEnabled(False)
913 923 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
914 924 return control
915 925
916 926 def _event_filter_console_keypress(self, event):
917 927 """ Filter key events for the underlying text widget to create a
918 928 console-like interface.
919 929 """
920 930 intercepted = False
921 931 cursor = self._control.textCursor()
922 932 position = cursor.position()
923 933 key = event.key()
924 934 ctrl_down = self._control_key_down(event.modifiers())
925 935 alt_down = event.modifiers() & QtCore.Qt.AltModifier
926 936 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
927 937
928 938 #------ Special sequences ----------------------------------------------
929 939
930 940 if event.matches(QtGui.QKeySequence.Copy):
931 941 self.copy()
932 942 intercepted = True
933 943
934 944 elif event.matches(QtGui.QKeySequence.Cut):
935 945 self.cut()
936 946 intercepted = True
937 947
938 948 elif event.matches(QtGui.QKeySequence.Paste):
939 949 self.paste()
940 950 intercepted = True
941 951
942 952 #------ Special modifier logic -----------------------------------------
943 953
944 954 elif key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
945 955 intercepted = True
946 956
947 957 # Special handling when tab completing in text mode.
948 958 self._cancel_text_completion()
949 959
950 960 if self._in_buffer(position):
951 961 if self._reading:
952 962 self._append_plain_text('\n')
953 963 self._reading = False
954 964 if self._reading_callback:
955 965 self._reading_callback()
956 966
957 967 # If the input buffer is a single line or there is only
958 968 # whitespace after the cursor, execute. Otherwise, split the
959 969 # line with a continuation prompt.
960 970 elif not self._executing:
961 971 cursor.movePosition(QtGui.QTextCursor.End,
962 972 QtGui.QTextCursor.KeepAnchor)
963 973 at_end = len(cursor.selectedText().strip()) == 0
964 974 single_line = (self._get_end_cursor().blockNumber() ==
965 975 self._get_prompt_cursor().blockNumber())
966 976 if (at_end or shift_down or single_line) and not ctrl_down:
967 977 self.execute(interactive = not shift_down)
968 978 else:
969 979 # Do this inside an edit block for clean undo/redo.
970 980 cursor.beginEditBlock()
971 981 cursor.setPosition(position)
972 982 cursor.insertText('\n')
973 983 self._insert_continuation_prompt(cursor)
974 984 cursor.endEditBlock()
975 985
976 986 # Ensure that the whole input buffer is visible.
977 987 # FIXME: This will not be usable if the input buffer is
978 988 # taller than the console widget.
979 989 self._control.moveCursor(QtGui.QTextCursor.End)
980 990 self._control.setTextCursor(cursor)
981 991
982 992 #------ Control/Cmd modifier -------------------------------------------
983 993
984 994 elif ctrl_down:
985 995 if key == QtCore.Qt.Key_G:
986 996 self._keyboard_quit()
987 997 intercepted = True
988 998
989 999 elif key == QtCore.Qt.Key_K:
990 1000 if self._in_buffer(position):
991 1001 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
992 1002 QtGui.QTextCursor.KeepAnchor)
993 1003 if not cursor.hasSelection():
994 1004 # Line deletion (remove continuation prompt)
995 1005 cursor.movePosition(QtGui.QTextCursor.NextBlock,
996 1006 QtGui.QTextCursor.KeepAnchor)
997 1007 cursor.movePosition(QtGui.QTextCursor.Right,
998 1008 QtGui.QTextCursor.KeepAnchor,
999 1009 len(self._continuation_prompt))
1000 1010 self._kill_ring.kill_cursor(cursor)
1001 1011 intercepted = True
1002 1012
1003 1013 elif key == QtCore.Qt.Key_L:
1004 1014 self.prompt_to_top()
1005 1015 intercepted = True
1006 1016
1007 1017 elif key == QtCore.Qt.Key_O:
1008 1018 if self._page_control and self._page_control.isVisible():
1009 1019 self._page_control.setFocus()
1010 1020 intercepted = True
1011 1021
1012 1022 elif key == QtCore.Qt.Key_U:
1013 1023 if self._in_buffer(position):
1014 1024 start_line = cursor.blockNumber()
1015 1025 if start_line == self._get_prompt_cursor().blockNumber():
1016 1026 offset = len(self._prompt)
1017 1027 else:
1018 1028 offset = len(self._continuation_prompt)
1019 1029 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1020 1030 QtGui.QTextCursor.KeepAnchor)
1021 1031 cursor.movePosition(QtGui.QTextCursor.Right,
1022 1032 QtGui.QTextCursor.KeepAnchor, offset)
1023 1033 self._kill_ring.kill_cursor(cursor)
1024 1034 intercepted = True
1025 1035
1026 1036 elif key == QtCore.Qt.Key_Y:
1027 1037 self._keep_cursor_in_buffer()
1028 1038 self._kill_ring.yank()
1029 1039 intercepted = True
1030 1040
1031 1041 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
1032 1042 intercepted = True
1033 1043
1034 1044 elif key in (QtCore.Qt.Key_Plus, QtCore.Qt.Key_Equal):
1035 1045 self.change_font_size(1)
1036 1046 intercepted = True
1037 1047
1038 1048 elif key == QtCore.Qt.Key_Minus:
1039 1049 self.change_font_size(-1)
1040 1050 intercepted = True
1041 1051
1042 1052 elif key == QtCore.Qt.Key_0:
1043 1053 self.reset_font()
1044 1054 intercepted = True
1045 1055
1046 1056 #------ Alt modifier ---------------------------------------------------
1047 1057
1048 1058 elif alt_down:
1049 1059 if key == QtCore.Qt.Key_B:
1050 1060 self._set_cursor(self._get_word_start_cursor(position))
1051 1061 intercepted = True
1052 1062
1053 1063 elif key == QtCore.Qt.Key_F:
1054 1064 self._set_cursor(self._get_word_end_cursor(position))
1055 1065 intercepted = True
1056 1066
1057 1067 elif key == QtCore.Qt.Key_Y:
1058 1068 self._kill_ring.rotate()
1059 1069 intercepted = True
1060 1070
1061 1071 elif key == QtCore.Qt.Key_Backspace:
1062 1072 cursor = self._get_word_start_cursor(position)
1063 1073 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1064 1074 self._kill_ring.kill_cursor(cursor)
1065 1075 intercepted = True
1066 1076
1067 1077 elif key == QtCore.Qt.Key_D:
1068 1078 cursor = self._get_word_end_cursor(position)
1069 1079 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1070 1080 self._kill_ring.kill_cursor(cursor)
1071 1081 intercepted = True
1072 1082
1073 1083 elif key == QtCore.Qt.Key_Delete:
1074 1084 intercepted = True
1075 1085
1076 1086 elif key == QtCore.Qt.Key_Greater:
1077 1087 self._control.moveCursor(QtGui.QTextCursor.End)
1078 1088 intercepted = True
1079 1089
1080 1090 elif key == QtCore.Qt.Key_Less:
1081 1091 self._control.setTextCursor(self._get_prompt_cursor())
1082 1092 intercepted = True
1083 1093
1084 1094 #------ No modifiers ---------------------------------------------------
1085 1095
1086 1096 else:
1087 1097 if shift_down:
1088 1098 anchormode = QtGui.QTextCursor.KeepAnchor
1089 1099 else:
1090 1100 anchormode = QtGui.QTextCursor.MoveAnchor
1091 1101
1092 1102 if key == QtCore.Qt.Key_Escape:
1093 1103 self._keyboard_quit()
1094 1104 intercepted = True
1095 1105
1096 1106 elif key == QtCore.Qt.Key_Up:
1097 1107 if self._reading or not self._up_pressed(shift_down):
1098 1108 intercepted = True
1099 1109 else:
1100 1110 prompt_line = self._get_prompt_cursor().blockNumber()
1101 1111 intercepted = cursor.blockNumber() <= prompt_line
1102 1112
1103 1113 elif key == QtCore.Qt.Key_Down:
1104 1114 if self._reading or not self._down_pressed(shift_down):
1105 1115 intercepted = True
1106 1116 else:
1107 1117 end_line = self._get_end_cursor().blockNumber()
1108 1118 intercepted = cursor.blockNumber() == end_line
1109 1119
1110 1120 elif key == QtCore.Qt.Key_Tab:
1111 1121 if not self._reading:
1112 1122 intercepted = not self._tab_pressed()
1113 1123
1114 1124 elif key == QtCore.Qt.Key_Left:
1115 1125
1116 1126 # Move to the previous line
1117 1127 line, col = cursor.blockNumber(), cursor.columnNumber()
1118 1128 if line > self._get_prompt_cursor().blockNumber() and \
1119 1129 col == len(self._continuation_prompt):
1120 1130 self._control.moveCursor(QtGui.QTextCursor.PreviousBlock,
1121 1131 mode=anchormode)
1122 1132 self._control.moveCursor(QtGui.QTextCursor.EndOfBlock,
1123 1133 mode=anchormode)
1124 1134 intercepted = True
1125 1135
1126 1136 # Regular left movement
1127 1137 else:
1128 1138 intercepted = not self._in_buffer(position - 1)
1129 1139
1130 1140 elif key == QtCore.Qt.Key_Right:
1131 1141 original_block_number = cursor.blockNumber()
1132 1142 cursor.movePosition(QtGui.QTextCursor.Right,
1133 1143 mode=anchormode)
1134 1144 if cursor.blockNumber() != original_block_number:
1135 1145 cursor.movePosition(QtGui.QTextCursor.Right,
1136 1146 n=len(self._continuation_prompt),
1137 1147 mode=anchormode)
1138 1148 self._set_cursor(cursor)
1139 1149 intercepted = True
1140 1150
1141 1151 elif key == QtCore.Qt.Key_Home:
1142 1152 start_line = cursor.blockNumber()
1143 1153 if start_line == self._get_prompt_cursor().blockNumber():
1144 1154 start_pos = self._prompt_pos
1145 1155 else:
1146 1156 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1147 1157 QtGui.QTextCursor.KeepAnchor)
1148 1158 start_pos = cursor.position()
1149 1159 start_pos += len(self._continuation_prompt)
1150 1160 cursor.setPosition(position)
1151 1161 if shift_down and self._in_buffer(position):
1152 1162 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
1153 1163 else:
1154 1164 cursor.setPosition(start_pos)
1155 1165 self._set_cursor(cursor)
1156 1166 intercepted = True
1157 1167
1158 1168 elif key == QtCore.Qt.Key_Backspace:
1159 1169
1160 1170 # Line deletion (remove continuation prompt)
1161 1171 line, col = cursor.blockNumber(), cursor.columnNumber()
1162 1172 if not self._reading and \
1163 1173 col == len(self._continuation_prompt) and \
1164 1174 line > self._get_prompt_cursor().blockNumber():
1165 1175 cursor.beginEditBlock()
1166 1176 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1167 1177 QtGui.QTextCursor.KeepAnchor)
1168 1178 cursor.removeSelectedText()
1169 1179 cursor.deletePreviousChar()
1170 1180 cursor.endEditBlock()
1171 1181 intercepted = True
1172 1182
1173 1183 # Regular backwards deletion
1174 1184 else:
1175 1185 anchor = cursor.anchor()
1176 1186 if anchor == position:
1177 1187 intercepted = not self._in_buffer(position - 1)
1178 1188 else:
1179 1189 intercepted = not self._in_buffer(min(anchor, position))
1180 1190
1181 1191 elif key == QtCore.Qt.Key_Delete:
1182 1192
1183 1193 # Line deletion (remove continuation prompt)
1184 1194 if not self._reading and self._in_buffer(position) and \
1185 1195 cursor.atBlockEnd() and not cursor.hasSelection():
1186 1196 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1187 1197 QtGui.QTextCursor.KeepAnchor)
1188 1198 cursor.movePosition(QtGui.QTextCursor.Right,
1189 1199 QtGui.QTextCursor.KeepAnchor,
1190 1200 len(self._continuation_prompt))
1191 1201 cursor.removeSelectedText()
1192 1202 intercepted = True
1193 1203
1194 1204 # Regular forwards deletion:
1195 1205 else:
1196 1206 anchor = cursor.anchor()
1197 1207 intercepted = (not self._in_buffer(anchor) or
1198 1208 not self._in_buffer(position))
1199 1209
1200 1210 # Don't move the cursor if Control/Cmd is pressed to allow copy-paste
1201 1211 # using the keyboard in any part of the buffer.
1202 1212 if not self._control_key_down(event.modifiers(), include_command=True):
1203 1213 self._keep_cursor_in_buffer()
1204 1214
1205 1215 return intercepted
1206 1216
1207 1217 def _event_filter_page_keypress(self, event):
1208 1218 """ Filter key events for the paging widget to create console-like
1209 1219 interface.
1210 1220 """
1211 1221 key = event.key()
1212 1222 ctrl_down = self._control_key_down(event.modifiers())
1213 1223 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1214 1224
1215 1225 if ctrl_down:
1216 1226 if key == QtCore.Qt.Key_O:
1217 1227 self._control.setFocus()
1218 1228 intercept = True
1219 1229
1220 1230 elif alt_down:
1221 1231 if key == QtCore.Qt.Key_Greater:
1222 1232 self._page_control.moveCursor(QtGui.QTextCursor.End)
1223 1233 intercepted = True
1224 1234
1225 1235 elif key == QtCore.Qt.Key_Less:
1226 1236 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1227 1237 intercepted = True
1228 1238
1229 1239 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
1230 1240 if self._splitter:
1231 1241 self._page_control.hide()
1232 1242 else:
1233 1243 self.layout().setCurrentWidget(self._control)
1234 1244 return True
1235 1245
1236 1246 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
1237 1247 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1238 1248 QtCore.Qt.Key_PageDown,
1239 1249 QtCore.Qt.NoModifier)
1240 1250 QtGui.qApp.sendEvent(self._page_control, new_event)
1241 1251 return True
1242 1252
1243 1253 elif key == QtCore.Qt.Key_Backspace:
1244 1254 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1245 1255 QtCore.Qt.Key_PageUp,
1246 1256 QtCore.Qt.NoModifier)
1247 1257 QtGui.qApp.sendEvent(self._page_control, new_event)
1248 1258 return True
1249 1259
1250 1260 return False
1251 1261
1252 1262 def _format_as_columns(self, items, separator=' '):
1253 1263 """ Transform a list of strings into a single string with columns.
1254 1264
1255 1265 Parameters
1256 1266 ----------
1257 1267 items : sequence of strings
1258 1268 The strings to process.
1259 1269
1260 1270 separator : str, optional [default is two spaces]
1261 1271 The string that separates columns.
1262 1272
1263 1273 Returns
1264 1274 -------
1265 1275 The formatted string.
1266 1276 """
1267 1277 # Note: this code is adapted from columnize 0.3.2.
1268 1278 # See http://code.google.com/p/pycolumnize/
1269 1279
1270 1280 # Calculate the number of characters available.
1271 1281 width = self._control.viewport().width()
1272 1282 char_width = QtGui.QFontMetrics(self.font).width(' ')
1273 1283 displaywidth = max(10, (width / char_width) - 1)
1274 1284
1275 1285 # Some degenerate cases.
1276 1286 size = len(items)
1277 1287 if size == 0:
1278 1288 return '\n'
1279 1289 elif size == 1:
1280 1290 return '%s\n' % items[0]
1281 1291
1282 1292 # Try every row count from 1 upwards
1283 1293 array_index = lambda nrows, row, col: nrows*col + row
1284 1294 for nrows in range(1, size):
1285 1295 ncols = (size + nrows - 1) // nrows
1286 1296 colwidths = []
1287 1297 totwidth = -len(separator)
1288 1298 for col in range(ncols):
1289 1299 # Get max column width for this column
1290 1300 colwidth = 0
1291 1301 for row in range(nrows):
1292 1302 i = array_index(nrows, row, col)
1293 1303 if i >= size: break
1294 1304 x = items[i]
1295 1305 colwidth = max(colwidth, len(x))
1296 1306 colwidths.append(colwidth)
1297 1307 totwidth += colwidth + len(separator)
1298 1308 if totwidth > displaywidth:
1299 1309 break
1300 1310 if totwidth <= displaywidth:
1301 1311 break
1302 1312
1303 1313 # The smallest number of rows computed and the max widths for each
1304 1314 # column has been obtained. Now we just have to format each of the rows.
1305 1315 string = ''
1306 1316 for row in range(nrows):
1307 1317 texts = []
1308 1318 for col in range(ncols):
1309 1319 i = row + nrows*col
1310 1320 if i >= size:
1311 1321 texts.append('')
1312 1322 else:
1313 1323 texts.append(items[i])
1314 1324 while texts and not texts[-1]:
1315 1325 del texts[-1]
1316 1326 for col in range(len(texts)):
1317 1327 texts[col] = texts[col].ljust(colwidths[col])
1318 1328 string += '%s\n' % separator.join(texts)
1319 1329 return string
1320 1330
1321 1331 def _get_block_plain_text(self, block):
1322 1332 """ Given a QTextBlock, return its unformatted text.
1323 1333 """
1324 1334 cursor = QtGui.QTextCursor(block)
1325 1335 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1326 1336 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1327 1337 QtGui.QTextCursor.KeepAnchor)
1328 1338 return cursor.selection().toPlainText()
1329 1339
1330 1340 def _get_cursor(self):
1331 1341 """ Convenience method that returns a cursor for the current position.
1332 1342 """
1333 1343 return self._control.textCursor()
1334 1344
1335 1345 def _get_end_cursor(self):
1336 1346 """ Convenience method that returns a cursor for the last character.
1337 1347 """
1338 1348 cursor = self._control.textCursor()
1339 1349 cursor.movePosition(QtGui.QTextCursor.End)
1340 1350 return cursor
1341 1351
1342 1352 def _get_input_buffer_cursor_column(self):
1343 1353 """ Returns the column of the cursor in the input buffer, excluding the
1344 1354 contribution by the prompt, or -1 if there is no such column.
1345 1355 """
1346 1356 prompt = self._get_input_buffer_cursor_prompt()
1347 1357 if prompt is None:
1348 1358 return -1
1349 1359 else:
1350 1360 cursor = self._control.textCursor()
1351 1361 return cursor.columnNumber() - len(prompt)
1352 1362
1353 1363 def _get_input_buffer_cursor_line(self):
1354 1364 """ Returns the text of the line of the input buffer that contains the
1355 1365 cursor, or None if there is no such line.
1356 1366 """
1357 1367 prompt = self._get_input_buffer_cursor_prompt()
1358 1368 if prompt is None:
1359 1369 return None
1360 1370 else:
1361 1371 cursor = self._control.textCursor()
1362 1372 text = self._get_block_plain_text(cursor.block())
1363 1373 return text[len(prompt):]
1364 1374
1365 1375 def _get_input_buffer_cursor_prompt(self):
1366 1376 """ Returns the (plain text) prompt for line of the input buffer that
1367 1377 contains the cursor, or None if there is no such line.
1368 1378 """
1369 1379 if self._executing:
1370 1380 return None
1371 1381 cursor = self._control.textCursor()
1372 1382 if cursor.position() >= self._prompt_pos:
1373 1383 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1374 1384 return self._prompt
1375 1385 else:
1376 1386 return self._continuation_prompt
1377 1387 else:
1378 1388 return None
1379 1389
1380 1390 def _get_prompt_cursor(self):
1381 1391 """ Convenience method that returns a cursor for the prompt position.
1382 1392 """
1383 1393 cursor = self._control.textCursor()
1384 1394 cursor.setPosition(self._prompt_pos)
1385 1395 return cursor
1386 1396
1387 1397 def _get_selection_cursor(self, start, end):
1388 1398 """ Convenience method that returns a cursor with text selected between
1389 1399 the positions 'start' and 'end'.
1390 1400 """
1391 1401 cursor = self._control.textCursor()
1392 1402 cursor.setPosition(start)
1393 1403 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1394 1404 return cursor
1395 1405
1396 1406 def _get_word_start_cursor(self, position):
1397 1407 """ Find the start of the word to the left the given position. If a
1398 1408 sequence of non-word characters precedes the first word, skip over
1399 1409 them. (This emulates the behavior of bash, emacs, etc.)
1400 1410 """
1401 1411 document = self._control.document()
1402 1412 position -= 1
1403 1413 while position >= self._prompt_pos and \
1404 1414 not is_letter_or_number(document.characterAt(position)):
1405 1415 position -= 1
1406 1416 while position >= self._prompt_pos and \
1407 1417 is_letter_or_number(document.characterAt(position)):
1408 1418 position -= 1
1409 1419 cursor = self._control.textCursor()
1410 1420 cursor.setPosition(position + 1)
1411 1421 return cursor
1412 1422
1413 1423 def _get_word_end_cursor(self, position):
1414 1424 """ Find the end of the word to the right the given position. If a
1415 1425 sequence of non-word characters precedes the first word, skip over
1416 1426 them. (This emulates the behavior of bash, emacs, etc.)
1417 1427 """
1418 1428 document = self._control.document()
1419 1429 end = self._get_end_cursor().position()
1420 1430 while position < end and \
1421 1431 not is_letter_or_number(document.characterAt(position)):
1422 1432 position += 1
1423 1433 while position < end and \
1424 1434 is_letter_or_number(document.characterAt(position)):
1425 1435 position += 1
1426 1436 cursor = self._control.textCursor()
1427 1437 cursor.setPosition(position)
1428 1438 return cursor
1429 1439
1430 1440 def _insert_continuation_prompt(self, cursor):
1431 1441 """ Inserts new continuation prompt using the specified cursor.
1432 1442 """
1433 1443 if self._continuation_prompt_html is None:
1434 1444 self._insert_plain_text(cursor, self._continuation_prompt)
1435 1445 else:
1436 1446 self._continuation_prompt = self._insert_html_fetching_plain_text(
1437 1447 cursor, self._continuation_prompt_html)
1438 1448
1439 1449 def _insert_html(self, cursor, html):
1440 1450 """ Inserts HTML using the specified cursor in such a way that future
1441 1451 formatting is unaffected.
1442 1452 """
1443 1453 cursor.beginEditBlock()
1444 1454 cursor.insertHtml(html)
1445 1455
1446 1456 # After inserting HTML, the text document "remembers" it's in "html
1447 1457 # mode", which means that subsequent calls adding plain text will result
1448 1458 # in unwanted formatting, lost tab characters, etc. The following code
1449 1459 # hacks around this behavior, which I consider to be a bug in Qt, by
1450 1460 # (crudely) resetting the document's style state.
1451 1461 cursor.movePosition(QtGui.QTextCursor.Left,
1452 1462 QtGui.QTextCursor.KeepAnchor)
1453 1463 if cursor.selection().toPlainText() == ' ':
1454 1464 cursor.removeSelectedText()
1455 1465 else:
1456 1466 cursor.movePosition(QtGui.QTextCursor.Right)
1457 1467 cursor.insertText(' ', QtGui.QTextCharFormat())
1458 1468 cursor.endEditBlock()
1459 1469
1460 1470 def _insert_html_fetching_plain_text(self, cursor, html):
1461 1471 """ Inserts HTML using the specified cursor, then returns its plain text
1462 1472 version.
1463 1473 """
1464 1474 cursor.beginEditBlock()
1465 1475 cursor.removeSelectedText()
1466 1476
1467 1477 start = cursor.position()
1468 1478 self._insert_html(cursor, html)
1469 1479 end = cursor.position()
1470 1480 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1471 1481 text = cursor.selection().toPlainText()
1472 1482
1473 1483 cursor.setPosition(end)
1474 1484 cursor.endEditBlock()
1475 1485 return text
1476 1486
1477 1487 def _insert_plain_text(self, cursor, text):
1478 1488 """ Inserts plain text using the specified cursor, processing ANSI codes
1479 1489 if enabled.
1480 1490 """
1481 1491 cursor.beginEditBlock()
1482 1492 if self.ansi_codes:
1483 1493 for substring in self._ansi_processor.split_string(text):
1484 1494 for act in self._ansi_processor.actions:
1485 1495
1486 1496 # Unlike real terminal emulators, we don't distinguish
1487 1497 # between the screen and the scrollback buffer. A screen
1488 1498 # erase request clears everything.
1489 1499 if act.action == 'erase' and act.area == 'screen':
1490 1500 cursor.select(QtGui.QTextCursor.Document)
1491 1501 cursor.removeSelectedText()
1492 1502
1493 1503 # Simulate a form feed by scrolling just past the last line.
1494 1504 elif act.action == 'scroll' and act.unit == 'page':
1495 1505 cursor.insertText('\n')
1496 1506 cursor.endEditBlock()
1497 1507 self._set_top_cursor(cursor)
1498 1508 cursor.joinPreviousEditBlock()
1499 1509 cursor.deletePreviousChar()
1500 1510
1501 1511 format = self._ansi_processor.get_format()
1502 1512 cursor.insertText(substring, format)
1503 1513 else:
1504 1514 cursor.insertText(text)
1505 1515 cursor.endEditBlock()
1506 1516
1507 1517 def _insert_plain_text_into_buffer(self, cursor, text):
1508 1518 """ Inserts text into the input buffer using the specified cursor (which
1509 1519 must be in the input buffer), ensuring that continuation prompts are
1510 1520 inserted as necessary.
1511 1521 """
1512 1522 lines = text.splitlines(True)
1513 1523 if lines:
1514 1524 cursor.beginEditBlock()
1515 1525 cursor.insertText(lines[0])
1516 1526 for line in lines[1:]:
1517 1527 if self._continuation_prompt_html is None:
1518 1528 cursor.insertText(self._continuation_prompt)
1519 1529 else:
1520 1530 self._continuation_prompt = \
1521 1531 self._insert_html_fetching_plain_text(
1522 1532 cursor, self._continuation_prompt_html)
1523 1533 cursor.insertText(line)
1524 1534 cursor.endEditBlock()
1525 1535
1526 1536 def _in_buffer(self, position=None):
1527 1537 """ Returns whether the current cursor (or, if specified, a position) is
1528 1538 inside the editing region.
1529 1539 """
1530 1540 cursor = self._control.textCursor()
1531 1541 if position is None:
1532 1542 position = cursor.position()
1533 1543 else:
1534 1544 cursor.setPosition(position)
1535 1545 line = cursor.blockNumber()
1536 1546 prompt_line = self._get_prompt_cursor().blockNumber()
1537 1547 if line == prompt_line:
1538 1548 return position >= self._prompt_pos
1539 1549 elif line > prompt_line:
1540 1550 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1541 1551 prompt_pos = cursor.position() + len(self._continuation_prompt)
1542 1552 return position >= prompt_pos
1543 1553 return False
1544 1554
1545 1555 def _keep_cursor_in_buffer(self):
1546 1556 """ Ensures that the cursor is inside the editing region. Returns
1547 1557 whether the cursor was moved.
1548 1558 """
1549 1559 moved = not self._in_buffer()
1550 1560 if moved:
1551 1561 cursor = self._control.textCursor()
1552 1562 cursor.movePosition(QtGui.QTextCursor.End)
1553 1563 self._control.setTextCursor(cursor)
1554 1564 return moved
1555 1565
1556 1566 def _keyboard_quit(self):
1557 1567 """ Cancels the current editing task ala Ctrl-G in Emacs.
1558 1568 """
1559 1569 if self._text_completing_pos:
1560 1570 self._cancel_text_completion()
1561 1571 else:
1562 1572 self.input_buffer = ''
1563 1573
1564 1574 def _page(self, text, html=False):
1565 1575 """ Displays text using the pager if it exceeds the height of the
1566 1576 viewport.
1567 1577
1568 1578 Parameters:
1569 1579 -----------
1570 1580 html : bool, optional (default False)
1571 1581 If set, the text will be interpreted as HTML instead of plain text.
1572 1582 """
1573 1583 line_height = QtGui.QFontMetrics(self.font).height()
1574 1584 minlines = self._control.viewport().height() / line_height
1575 1585 if self.paging != 'none' and \
1576 1586 re.match("(?:[^\n]*\n){%i}" % minlines, text):
1577 1587 if self.paging == 'custom':
1578 1588 self.custom_page_requested.emit(text)
1579 1589 else:
1580 1590 self._page_control.clear()
1581 1591 cursor = self._page_control.textCursor()
1582 1592 if html:
1583 1593 self._insert_html(cursor, text)
1584 1594 else:
1585 1595 self._insert_plain_text(cursor, text)
1586 1596 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1587 1597
1588 1598 self._page_control.viewport().resize(self._control.size())
1589 1599 if self._splitter:
1590 1600 self._page_control.show()
1591 1601 self._page_control.setFocus()
1592 1602 else:
1593 1603 self.layout().setCurrentWidget(self._page_control)
1594 1604 elif html:
1595 1605 self._append_plain_html(text)
1596 1606 else:
1597 1607 self._append_plain_text(text)
1598 1608
1599 1609 def _prompt_finished(self):
1600 1610 """ Called immediately after a prompt is finished, i.e. when some input
1601 1611 will be processed and a new prompt displayed.
1602 1612 """
1603 1613 self._control.setReadOnly(True)
1604 1614 self._prompt_finished_hook()
1605 1615
1606 1616 def _prompt_started(self):
1607 1617 """ Called immediately after a new prompt is displayed.
1608 1618 """
1609 1619 # Temporarily disable the maximum block count to permit undo/redo and
1610 1620 # to ensure that the prompt position does not change due to truncation.
1611 1621 self._control.document().setMaximumBlockCount(0)
1612 1622 self._control.setUndoRedoEnabled(True)
1613 1623
1614 1624 # Work around bug in QPlainTextEdit: input method is not re-enabled
1615 1625 # when read-only is disabled.
1616 1626 self._control.setReadOnly(False)
1617 1627 self._control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1618 1628
1619 1629 self._executing = False
1620 1630 self._prompt_started_hook()
1621 1631
1622 1632 # If the input buffer has changed while executing, load it.
1623 1633 if self._input_buffer_pending:
1624 1634 self.input_buffer = self._input_buffer_pending
1625 1635 self._input_buffer_pending = ''
1626 1636
1627 1637 self._control.moveCursor(QtGui.QTextCursor.End)
1628 1638
1629 1639 def _readline(self, prompt='', callback=None):
1630 1640 """ Reads one line of input from the user.
1631 1641
1632 1642 Parameters
1633 1643 ----------
1634 1644 prompt : str, optional
1635 1645 The prompt to print before reading the line.
1636 1646
1637 1647 callback : callable, optional
1638 1648 A callback to execute with the read line. If not specified, input is
1639 1649 read *synchronously* and this method does not return until it has
1640 1650 been read.
1641 1651
1642 1652 Returns
1643 1653 -------
1644 1654 If a callback is specified, returns nothing. Otherwise, returns the
1645 1655 input string with the trailing newline stripped.
1646 1656 """
1647 1657 if self._reading:
1648 1658 raise RuntimeError('Cannot read a line. Widget is already reading.')
1649 1659
1650 1660 if not callback and not self.isVisible():
1651 1661 # If the user cannot see the widget, this function cannot return.
1652 1662 raise RuntimeError('Cannot synchronously read a line if the widget '
1653 1663 'is not visible!')
1654 1664
1655 1665 self._reading = True
1656 1666 self._show_prompt(prompt, newline=False)
1657 1667
1658 1668 if callback is None:
1659 1669 self._reading_callback = None
1660 1670 while self._reading:
1661 1671 QtCore.QCoreApplication.processEvents()
1662 1672 return self.input_buffer.rstrip('\n')
1663 1673
1664 1674 else:
1665 1675 self._reading_callback = lambda: \
1666 1676 callback(self.input_buffer.rstrip('\n'))
1667 1677
1668 1678 def _set_continuation_prompt(self, prompt, html=False):
1669 1679 """ Sets the continuation prompt.
1670 1680
1671 1681 Parameters
1672 1682 ----------
1673 1683 prompt : str
1674 1684 The prompt to show when more input is needed.
1675 1685
1676 1686 html : bool, optional (default False)
1677 1687 If set, the prompt will be inserted as formatted HTML. Otherwise,
1678 1688 the prompt will be treated as plain text, though ANSI color codes
1679 1689 will be handled.
1680 1690 """
1681 1691 if html:
1682 1692 self._continuation_prompt_html = prompt
1683 1693 else:
1684 1694 self._continuation_prompt = prompt
1685 1695 self._continuation_prompt_html = None
1686 1696
1687 1697 def _set_cursor(self, cursor):
1688 1698 """ Convenience method to set the current cursor.
1689 1699 """
1690 1700 self._control.setTextCursor(cursor)
1691 1701
1692 1702 def _set_top_cursor(self, cursor):
1693 1703 """ Scrolls the viewport so that the specified cursor is at the top.
1694 1704 """
1695 1705 scrollbar = self._control.verticalScrollBar()
1696 1706 scrollbar.setValue(scrollbar.maximum())
1697 1707 original_cursor = self._control.textCursor()
1698 1708 self._control.setTextCursor(cursor)
1699 1709 self._control.ensureCursorVisible()
1700 1710 self._control.setTextCursor(original_cursor)
1701 1711
1702 1712 def _show_prompt(self, prompt=None, html=False, newline=True):
1703 1713 """ Writes a new prompt at the end of the buffer.
1704 1714
1705 1715 Parameters
1706 1716 ----------
1707 1717 prompt : str, optional
1708 1718 The prompt to show. If not specified, the previous prompt is used.
1709 1719
1710 1720 html : bool, optional (default False)
1711 1721 Only relevant when a prompt is specified. If set, the prompt will
1712 1722 be inserted as formatted HTML. Otherwise, the prompt will be treated
1713 1723 as plain text, though ANSI color codes will be handled.
1714 1724
1715 1725 newline : bool, optional (default True)
1716 1726 If set, a new line will be written before showing the prompt if
1717 1727 there is not already a newline at the end of the buffer.
1718 1728 """
1719 1729 # Insert a preliminary newline, if necessary.
1720 1730 if newline:
1721 1731 cursor = self._get_end_cursor()
1722 1732 if cursor.position() > 0:
1723 1733 cursor.movePosition(QtGui.QTextCursor.Left,
1724 1734 QtGui.QTextCursor.KeepAnchor)
1725 1735 if cursor.selection().toPlainText() != '\n':
1726 1736 self._append_plain_text('\n')
1727 1737
1728 1738 # Write the prompt.
1729 1739 self._append_plain_text(self._prompt_sep)
1730 1740 if prompt is None:
1731 1741 if self._prompt_html is None:
1732 1742 self._append_plain_text(self._prompt)
1733 1743 else:
1734 1744 self._append_html(self._prompt_html)
1735 1745 else:
1736 1746 if html:
1737 1747 self._prompt = self._append_html_fetching_plain_text(prompt)
1738 1748 self._prompt_html = prompt
1739 1749 else:
1740 1750 self._append_plain_text(prompt)
1741 1751 self._prompt = prompt
1742 1752 self._prompt_html = None
1743 1753
1744 1754 self._prompt_pos = self._get_end_cursor().position()
1745 1755 self._prompt_started()
1746 1756
1747 1757 #------ Signal handlers ----------------------------------------------------
1748 1758
1749 1759 def _adjust_scrollbars(self):
1750 1760 """ Expands the vertical scrollbar beyond the range set by Qt.
1751 1761 """
1752 1762 # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
1753 1763 # and qtextedit.cpp.
1754 1764 document = self._control.document()
1755 1765 scrollbar = self._control.verticalScrollBar()
1756 1766 viewport_height = self._control.viewport().height()
1757 1767 if isinstance(self._control, QtGui.QPlainTextEdit):
1758 1768 maximum = max(0, document.lineCount() - 1)
1759 1769 step = viewport_height / self._control.fontMetrics().lineSpacing()
1760 1770 else:
1761 1771 # QTextEdit does not do line-based layout and blocks will not in
1762 1772 # general have the same height. Therefore it does not make sense to
1763 1773 # attempt to scroll in line height increments.
1764 1774 maximum = document.size().height()
1765 1775 step = viewport_height
1766 1776 diff = maximum - scrollbar.maximum()
1767 1777 scrollbar.setRange(0, maximum)
1768 1778 scrollbar.setPageStep(step)
1769 1779
1770 1780 # Compensate for undesirable scrolling that occurs automatically due to
1771 1781 # maximumBlockCount() text truncation.
1772 1782 if diff < 0 and document.blockCount() == document.maximumBlockCount():
1773 1783 scrollbar.setValue(scrollbar.value() + diff)
1774 1784
1775 1785 def _cursor_position_changed(self):
1776 1786 """ Clears the temporary buffer based on the cursor position.
1777 1787 """
1778 1788 if self._text_completing_pos:
1779 1789 document = self._control.document()
1780 1790 if self._text_completing_pos < document.characterCount():
1781 1791 cursor = self._control.textCursor()
1782 1792 pos = cursor.position()
1783 1793 text_cursor = self._control.textCursor()
1784 1794 text_cursor.setPosition(self._text_completing_pos)
1785 1795 if pos < self._text_completing_pos or \
1786 1796 cursor.blockNumber() > text_cursor.blockNumber():
1787 1797 self._clear_temporary_buffer()
1788 1798 self._text_completing_pos = 0
1789 1799 else:
1790 1800 self._clear_temporary_buffer()
1791 1801 self._text_completing_pos = 0
1792 1802
1793 1803 def _custom_context_menu_requested(self, pos):
1794 1804 """ Shows a context menu at the given QPoint (in widget coordinates).
1795 1805 """
1796 1806 menu = self._context_menu_make(pos)
1797 1807 menu.exec_(self._control.mapToGlobal(pos))
@@ -1,626 +1,620 b''
1 1 from __future__ import print_function
2 2
3 3 # Standard library imports
4 4 from collections import namedtuple
5 5 import sys
6 6 import time
7 7
8 8 # System library imports
9 9 from pygments.lexers import PythonLexer
10 10 from IPython.external.qt import QtCore, QtGui
11 11
12 12 # Local imports
13 13 from IPython.core.inputsplitter import InputSplitter, transform_classic_prompt
14 14 from IPython.core.oinspect import call_tip
15 15 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
16 16 from IPython.utils.traitlets import Bool, Instance
17 17 from bracket_matcher import BracketMatcher
18 18 from call_tip_widget import CallTipWidget
19 19 from completion_lexer import CompletionLexer
20 20 from history_console_widget import HistoryConsoleWidget
21 21 from pygments_highlighter import PygmentsHighlighter
22 22
23 23
24 24 class FrontendHighlighter(PygmentsHighlighter):
25 25 """ A PygmentsHighlighter that can be turned on and off and that ignores
26 26 prompts.
27 27 """
28 28
29 29 def __init__(self, frontend):
30 30 super(FrontendHighlighter, self).__init__(frontend._control.document())
31 31 self._current_offset = 0
32 32 self._frontend = frontend
33 33 self.highlighting_on = False
34 34
35 35 def highlightBlock(self, string):
36 36 """ Highlight a block of text. Reimplemented to highlight selectively.
37 37 """
38 38 if not self.highlighting_on:
39 39 return
40 40
41 41 # The input to this function is a unicode string that may contain
42 42 # paragraph break characters, non-breaking spaces, etc. Here we acquire
43 43 # the string as plain text so we can compare it.
44 44 current_block = self.currentBlock()
45 45 string = self._frontend._get_block_plain_text(current_block)
46 46
47 47 # Decide whether to check for the regular or continuation prompt.
48 48 if current_block.contains(self._frontend._prompt_pos):
49 49 prompt = self._frontend._prompt
50 50 else:
51 51 prompt = self._frontend._continuation_prompt
52 52
53 53 # Don't highlight the part of the string that contains the prompt.
54 54 if string.startswith(prompt):
55 55 self._current_offset = len(prompt)
56 56 string = string[len(prompt):]
57 57 else:
58 58 self._current_offset = 0
59 59
60 60 PygmentsHighlighter.highlightBlock(self, string)
61 61
62 62 def rehighlightBlock(self, block):
63 63 """ Reimplemented to temporarily enable highlighting if disabled.
64 64 """
65 65 old = self.highlighting_on
66 66 self.highlighting_on = True
67 67 super(FrontendHighlighter, self).rehighlightBlock(block)
68 68 self.highlighting_on = old
69 69
70 70 def setFormat(self, start, count, format):
71 71 """ Reimplemented to highlight selectively.
72 72 """
73 73 start += self._current_offset
74 74 PygmentsHighlighter.setFormat(self, start, count, format)
75 75
76 76
77 77 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
78 78 """ A Qt frontend for a generic Python kernel.
79 79 """
80 80
81 81 # An option and corresponding signal for overriding the default kernel
82 82 # interrupt behavior.
83 83 custom_interrupt = Bool(False)
84 84 custom_interrupt_requested = QtCore.Signal()
85 85
86 86 # An option and corresponding signals for overriding the default kernel
87 87 # restart behavior.
88 88 custom_restart = Bool(False)
89 89 custom_restart_kernel_died = QtCore.Signal(float)
90 90 custom_restart_requested = QtCore.Signal()
91 91
92 92 # Emitted when a user visible 'execute_request' has been submitted to the
93 93 # kernel from the FrontendWidget. Contains the code to be executed.
94 94 executing = QtCore.Signal(object)
95 95
96 96 # Emitted when a user-visible 'execute_reply' has been received from the
97 97 # kernel and processed by the FrontendWidget. Contains the response message.
98 98 executed = QtCore.Signal(object)
99 99
100 100 # Emitted when an exit request has been received from the kernel.
101 101 exit_requested = QtCore.Signal()
102 102
103 103 # Protected class variables.
104 104 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
105 105 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
106 106 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
107 107 _input_splitter_class = InputSplitter
108 108 _local_kernel = False
109 109 _highlighter = Instance(FrontendHighlighter)
110 110
111 111 #---------------------------------------------------------------------------
112 112 # 'object' interface
113 113 #---------------------------------------------------------------------------
114 114
115 115 def __init__(self, *args, **kw):
116 116 super(FrontendWidget, self).__init__(*args, **kw)
117 117
118 118 # FrontendWidget protected variables.
119 119 self._bracket_matcher = BracketMatcher(self._control)
120 120 self._call_tip_widget = CallTipWidget(self._control)
121 121 self._completion_lexer = CompletionLexer(PythonLexer())
122 122 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
123 123 self._hidden = False
124 124 self._highlighter = FrontendHighlighter(self)
125 125 self._input_splitter = self._input_splitter_class(input_mode='cell')
126 126 self._kernel_manager = None
127 127 self._request_info = {}
128 128
129 129 # Configure the ConsoleWidget.
130 130 self.tab_width = 4
131 131 self._set_continuation_prompt('... ')
132 132
133 133 # Configure the CallTipWidget.
134 134 self._call_tip_widget.setFont(self.font)
135 135 self.font_changed.connect(self._call_tip_widget.setFont)
136 136
137 137 # Configure actions.
138 138 action = self._copy_raw_action
139 139 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
140 140 action.setEnabled(False)
141 141 action.setShortcut(QtGui.QKeySequence(key))
142 142 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
143 143 action.triggered.connect(self.copy_raw)
144 144 self.copy_available.connect(action.setEnabled)
145 145 self.addAction(action)
146 146
147 147 # Connect signal handlers.
148 148 document = self._control.document()
149 149 document.contentsChange.connect(self._document_contents_change)
150 150
151 151 # Set flag for whether we are connected via localhost.
152 152 self._local_kernel = kw.get('local_kernel',
153 153 FrontendWidget._local_kernel)
154 154
155 155 #---------------------------------------------------------------------------
156 156 # 'ConsoleWidget' public interface
157 157 #---------------------------------------------------------------------------
158 158
159 159 def copy(self):
160 160 """ Copy the currently selected text to the clipboard, removing prompts.
161 161 """
162 162 text = self._control.textCursor().selection().toPlainText()
163 163 if text:
164 164 lines = map(transform_classic_prompt, text.splitlines())
165 165 text = '\n'.join(lines)
166 166 QtGui.QApplication.clipboard().setText(text)
167 167
168 168 #---------------------------------------------------------------------------
169 169 # 'ConsoleWidget' abstract interface
170 170 #---------------------------------------------------------------------------
171 171
172 172 def _is_complete(self, source, interactive):
173 173 """ Returns whether 'source' can be completely processed and a new
174 174 prompt created. When triggered by an Enter/Return key press,
175 175 'interactive' is True; otherwise, it is False.
176 176 """
177 177 complete = self._input_splitter.push(source)
178 178 if interactive:
179 179 complete = not self._input_splitter.push_accepts_more()
180 180 return complete
181 181
182 182 def _execute(self, source, hidden):
183 183 """ Execute 'source'. If 'hidden', do not show any output.
184 184
185 185 See parent class :meth:`execute` docstring for full details.
186 186 """
187 187 msg_id = self.kernel_manager.shell_channel.execute(source, hidden)
188 188 self._request_info['execute'] = self._ExecutionRequest(msg_id, 'user')
189 189 self._hidden = hidden
190 190 if not hidden:
191 191 self.executing.emit(source)
192 192
193 193 def _prompt_started_hook(self):
194 194 """ Called immediately after a new prompt is displayed.
195 195 """
196 196 if not self._reading:
197 197 self._highlighter.highlighting_on = True
198 198
199 199 def _prompt_finished_hook(self):
200 200 """ Called immediately after a prompt is finished, i.e. when some input
201 201 will be processed and a new prompt displayed.
202 202 """
203 203 # Flush all state from the input splitter so the next round of
204 204 # reading input starts with a clean buffer.
205 205 self._input_splitter.reset()
206 206
207 207 if not self._reading:
208 208 self._highlighter.highlighting_on = False
209 209
210 210 def _tab_pressed(self):
211 211 """ Called when the tab key is pressed. Returns whether to continue
212 212 processing the event.
213 213 """
214 214 # Perform tab completion if:
215 215 # 1) The cursor is in the input buffer.
216 216 # 2) There is a non-whitespace character before the cursor.
217 217 text = self._get_input_buffer_cursor_line()
218 218 if text is None:
219 219 return False
220 220 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
221 221 if complete:
222 222 self._complete()
223 223 return not complete
224 224
225 225 #---------------------------------------------------------------------------
226 226 # 'ConsoleWidget' protected interface
227 227 #---------------------------------------------------------------------------
228 228
229 229 def _context_menu_make(self, pos):
230 230 """ Reimplemented to add an action for raw copy.
231 231 """
232 232 menu = super(FrontendWidget, self)._context_menu_make(pos)
233 233 for before_action in menu.actions():
234 234 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
235 235 QtGui.QKeySequence.ExactMatch:
236 236 menu.insertAction(before_action, self._copy_raw_action)
237 237 break
238 238 return menu
239 239
240 240 def _event_filter_console_keypress(self, event):
241 241 """ Reimplemented for execution interruption and smart backspace.
242 242 """
243 243 key = event.key()
244 244 if self._control_key_down(event.modifiers(), include_command=False):
245 245
246 246 if key == QtCore.Qt.Key_C and self._executing:
247 247 self.interrupt_kernel()
248 248 return True
249 249
250 250 elif key == QtCore.Qt.Key_Period:
251 251 message = 'Are you sure you want to restart the kernel?'
252 252 self.restart_kernel(message, now=False)
253 253 return True
254 254
255 255 elif not event.modifiers() & QtCore.Qt.AltModifier:
256 256
257 257 # Smart backspace: remove four characters in one backspace if:
258 258 # 1) everything left of the cursor is whitespace
259 259 # 2) the four characters immediately left of the cursor are spaces
260 260 if key == QtCore.Qt.Key_Backspace:
261 261 col = self._get_input_buffer_cursor_column()
262 262 cursor = self._control.textCursor()
263 263 if col > 3 and not cursor.hasSelection():
264 264 text = self._get_input_buffer_cursor_line()[:col]
265 265 if text.endswith(' ') and not text.strip():
266 266 cursor.movePosition(QtGui.QTextCursor.Left,
267 267 QtGui.QTextCursor.KeepAnchor, 4)
268 268 cursor.removeSelectedText()
269 269 return True
270 270
271 271 return super(FrontendWidget, self)._event_filter_console_keypress(event)
272 272
273 273 def _insert_continuation_prompt(self, cursor):
274 274 """ Reimplemented for auto-indentation.
275 275 """
276 276 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
277 277 cursor.insertText(' ' * self._input_splitter.indent_spaces)
278 278
279 279 #---------------------------------------------------------------------------
280 280 # 'BaseFrontendMixin' abstract interface
281 281 #---------------------------------------------------------------------------
282 282
283 283 def _handle_complete_reply(self, rep):
284 284 """ Handle replies for tab completion.
285 285 """
286 286 cursor = self._get_cursor()
287 287 info = self._request_info.get('complete')
288 288 if info and info.id == rep['parent_header']['msg_id'] and \
289 289 info.pos == cursor.position():
290 290 text = '.'.join(self._get_context())
291 291 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
292 292 self._complete_with_items(cursor, rep['content']['matches'])
293 293
294 294 def _handle_execute_reply(self, msg):
295 295 """ Handles replies for code execution.
296 296 """
297 297 info = self._request_info.get('execute')
298 298 if info and info.id == msg['parent_header']['msg_id'] and \
299 299 info.kind == 'user' and not self._hidden:
300 300 # Make sure that all output from the SUB channel has been processed
301 301 # before writing a new prompt.
302 302 self.kernel_manager.sub_channel.flush()
303 303
304 304 # Reset the ANSI style information to prevent bad text in stdout
305 305 # from messing up our colors. We're not a true terminal so we're
306 306 # allowed to do this.
307 307 if self.ansi_codes:
308 308 self._ansi_processor.reset_sgr()
309 309
310 310 content = msg['content']
311 311 status = content['status']
312 312 if status == 'ok':
313 313 self._process_execute_ok(msg)
314 314 elif status == 'error':
315 315 self._process_execute_error(msg)
316 316 elif status == 'abort':
317 317 self._process_execute_abort(msg)
318 318
319 319 self._show_interpreter_prompt_for_reply(msg)
320 320 self.executed.emit(msg)
321 321
322 322 def _handle_input_request(self, msg):
323 323 """ Handle requests for raw_input.
324 324 """
325 325 if self._hidden:
326 326 raise RuntimeError('Request for raw input during hidden execution.')
327 327
328 328 # Make sure that all output from the SUB channel has been processed
329 329 # before entering readline mode.
330 330 self.kernel_manager.sub_channel.flush()
331 331
332 332 def callback(line):
333 333 self.kernel_manager.stdin_channel.input(line)
334 334 self._readline(msg['content']['prompt'], callback=callback)
335 335
336 336 def _handle_kernel_died(self, since_last_heartbeat):
337 337 """ Handle the kernel's death by asking if the user wants to restart.
338 338 """
339 339 if self.custom_restart:
340 340 self.custom_restart_kernel_died.emit(since_last_heartbeat)
341 341 else:
342 342 message = 'The kernel heartbeat has been inactive for %.2f ' \
343 343 'seconds. Do you want to restart the kernel? You may ' \
344 344 'first want to check the network connection.' % \
345 345 since_last_heartbeat
346 346 self.restart_kernel(message, now=True)
347 347
348 348 def _handle_object_info_reply(self, rep):
349 349 """ Handle replies for call tips.
350 350 """
351 351 cursor = self._get_cursor()
352 352 info = self._request_info.get('call_tip')
353 353 if info and info.id == rep['parent_header']['msg_id'] and \
354 354 info.pos == cursor.position():
355 355 # Get the information for a call tip. For now we format the call
356 356 # line as string, later we can pass False to format_call and
357 357 # syntax-highlight it ourselves for nicer formatting in the
358 358 # calltip.
359 359 content = rep['content']
360 360 # if this is from pykernel, 'docstring' will be the only key
361 361 if content.get('ismagic', False):
362 362 # Don't generate a call-tip for magics. Ideally, we should
363 363 # generate a tooltip, but not on ( like we do for actual
364 364 # callables.
365 365 call_info, doc = None, None
366 366 else:
367 367 call_info, doc = call_tip(content, format_call=True)
368 368 if call_info or doc:
369 369 self._call_tip_widget.show_call_info(call_info, doc)
370 370
371 371 def _handle_pyout(self, msg):
372 372 """ Handle display hook output.
373 373 """
374 374 if not self._hidden and self._is_from_this_session(msg):
375 data = msg['content']['data']
376 if isinstance(data, basestring):
377 # plaintext data from pure Python kernel
378 text = data
379 else:
380 # formatted output from DisplayFormatter (IPython kernel)
381 text = data.get('text/plain', '')
382 self._append_plain_text(text + '\n')
375 text = msg['content']['data']
376 self._append_plain_text(text + '\n', before_prompt=True)
383 377
384 378 def _handle_stream(self, msg):
385 379 """ Handle stdout, stderr, and stdin.
386 380 """
387 381 if not self._hidden and self._is_from_this_session(msg):
388 382 # Most consoles treat tabs as being 8 space characters. Convert tabs
389 383 # to spaces so that output looks as expected regardless of this
390 384 # widget's tab width.
391 385 text = msg['content']['data'].expandtabs(8)
392 386
393 self._append_plain_text(text)
387 self._append_plain_text(text, before_prompt=True)
394 388 self._control.moveCursor(QtGui.QTextCursor.End)
395 389
396 390 def _handle_shutdown_reply(self, msg):
397 391 """ Handle shutdown signal, only if from other console.
398 392 """
399 393 if not self._hidden and not self._is_from_this_session(msg):
400 394 if self._local_kernel:
401 395 if not msg['content']['restart']:
402 396 sys.exit(0)
403 397 else:
404 398 # we just got notified of a restart!
405 399 time.sleep(0.25) # wait 1/4 sec to reset
406 400 # lest the request for a new prompt
407 401 # goes to the old kernel
408 402 self.reset()
409 403 else: # remote kernel, prompt on Kernel shutdown/reset
410 404 title = self.window().windowTitle()
411 405 if not msg['content']['restart']:
412 406 reply = QtGui.QMessageBox.question(self, title,
413 407 "Kernel has been shutdown permanently. "
414 408 "Close the Console?",
415 409 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
416 410 if reply == QtGui.QMessageBox.Yes:
417 411 sys.exit(0)
418 412 else:
419 413 reply = QtGui.QMessageBox.question(self, title,
420 414 "Kernel has been reset. Clear the Console?",
421 415 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
422 416 if reply == QtGui.QMessageBox.Yes:
423 417 time.sleep(0.25) # wait 1/4 sec to reset
424 418 # lest the request for a new prompt
425 419 # goes to the old kernel
426 420 self.reset()
427 421
428 422 def _started_channels(self):
429 423 """ Called when the KernelManager channels have started listening or
430 424 when the frontend is assigned an already listening KernelManager.
431 425 """
432 426 self.reset()
433 427
434 428 #---------------------------------------------------------------------------
435 429 # 'FrontendWidget' public interface
436 430 #---------------------------------------------------------------------------
437 431
438 432 def copy_raw(self):
439 433 """ Copy the currently selected text to the clipboard without attempting
440 434 to remove prompts or otherwise alter the text.
441 435 """
442 436 self._control.copy()
443 437
444 438 def execute_file(self, path, hidden=False):
445 439 """ Attempts to execute file with 'path'. If 'hidden', no output is
446 440 shown.
447 441 """
448 442 self.execute('execfile(%r)' % path, hidden=hidden)
449 443
450 444 def interrupt_kernel(self):
451 445 """ Attempts to interrupt the running kernel.
452 446 """
453 447 if self.custom_interrupt:
454 448 self.custom_interrupt_requested.emit()
455 449 elif self.kernel_manager.has_kernel:
456 450 self.kernel_manager.interrupt_kernel()
457 451 else:
458 452 self._append_plain_text('Kernel process is either remote or '
459 453 'unspecified. Cannot interrupt.\n')
460 454
461 455 def reset(self):
462 456 """ Resets the widget to its initial state. Similar to ``clear``, but
463 457 also re-writes the banner and aborts execution if necessary.
464 458 """
465 459 if self._executing:
466 460 self._executing = False
467 461 self._request_info['execute'] = None
468 462 self._reading = False
469 463 self._highlighter.highlighting_on = False
470 464
471 465 self._control.clear()
472 466 self._append_plain_text(self._get_banner())
473 467 self._show_interpreter_prompt()
474 468
475 469 def restart_kernel(self, message, now=False):
476 470 """ Attempts to restart the running kernel.
477 471 """
478 472 # FIXME: now should be configurable via a checkbox in the dialog. Right
479 473 # now at least the heartbeat path sets it to True and the manual restart
480 474 # to False. But those should just be the pre-selected states of a
481 475 # checkbox that the user could override if so desired. But I don't know
482 476 # enough Qt to go implementing the checkbox now.
483 477
484 478 if self.custom_restart:
485 479 self.custom_restart_requested.emit()
486 480
487 481 elif self.kernel_manager.has_kernel:
488 482 # Pause the heart beat channel to prevent further warnings.
489 483 self.kernel_manager.hb_channel.pause()
490 484
491 485 # Prompt the user to restart the kernel. Un-pause the heartbeat if
492 486 # they decline. (If they accept, the heartbeat will be un-paused
493 487 # automatically when the kernel is restarted.)
494 488 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
495 489 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
496 490 message, buttons)
497 491 if result == QtGui.QMessageBox.Yes:
498 492 try:
499 493 self.kernel_manager.restart_kernel(now=now)
500 494 except RuntimeError:
501 495 self._append_plain_text('Kernel started externally. '
502 496 'Cannot restart.\n')
503 497 else:
504 498 self.reset()
505 499 else:
506 500 self.kernel_manager.hb_channel.unpause()
507 501
508 502 else:
509 503 self._append_plain_text('Kernel process is either remote or '
510 504 'unspecified. Cannot restart.\n')
511 505
512 506 #---------------------------------------------------------------------------
513 507 # 'FrontendWidget' protected interface
514 508 #---------------------------------------------------------------------------
515 509
516 510 def _call_tip(self):
517 511 """ Shows a call tip, if appropriate, at the current cursor location.
518 512 """
519 513 # Decide if it makes sense to show a call tip
520 514 cursor = self._get_cursor()
521 515 cursor.movePosition(QtGui.QTextCursor.Left)
522 516 if cursor.document().characterAt(cursor.position()) != '(':
523 517 return False
524 518 context = self._get_context(cursor)
525 519 if not context:
526 520 return False
527 521
528 522 # Send the metadata request to the kernel
529 523 name = '.'.join(context)
530 524 msg_id = self.kernel_manager.shell_channel.object_info(name)
531 525 pos = self._get_cursor().position()
532 526 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
533 527 return True
534 528
535 529 def _complete(self):
536 530 """ Performs completion at the current cursor location.
537 531 """
538 532 context = self._get_context()
539 533 if context:
540 534 # Send the completion request to the kernel
541 535 msg_id = self.kernel_manager.shell_channel.complete(
542 536 '.'.join(context), # text
543 537 self._get_input_buffer_cursor_line(), # line
544 538 self._get_input_buffer_cursor_column(), # cursor_pos
545 539 self.input_buffer) # block
546 540 pos = self._get_cursor().position()
547 541 info = self._CompletionRequest(msg_id, pos)
548 542 self._request_info['complete'] = info
549 543
550 544 def _get_banner(self):
551 545 """ Gets a banner to display at the beginning of a session.
552 546 """
553 547 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
554 548 '"license" for more information.'
555 549 return banner % (sys.version, sys.platform)
556 550
557 551 def _get_context(self, cursor=None):
558 552 """ Gets the context for the specified cursor (or the current cursor
559 553 if none is specified).
560 554 """
561 555 if cursor is None:
562 556 cursor = self._get_cursor()
563 557 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
564 558 QtGui.QTextCursor.KeepAnchor)
565 559 text = cursor.selection().toPlainText()
566 560 return self._completion_lexer.get_context(text)
567 561
568 562 def _process_execute_abort(self, msg):
569 563 """ Process a reply for an aborted execution request.
570 564 """
571 565 self._append_plain_text("ERROR: execution aborted\n")
572 566
573 567 def _process_execute_error(self, msg):
574 568 """ Process a reply for an execution request that resulted in an error.
575 569 """
576 570 content = msg['content']
577 571 # If a SystemExit is passed along, this means exit() was called - also
578 572 # all the ipython %exit magic syntax of '-k' to be used to keep
579 573 # the kernel running
580 574 if content['ename']=='SystemExit':
581 575 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
582 576 self._keep_kernel_on_exit = keepkernel
583 577 self.exit_requested.emit()
584 578 else:
585 579 traceback = ''.join(content['traceback'])
586 580 self._append_plain_text(traceback)
587 581
588 582 def _process_execute_ok(self, msg):
589 583 """ Process a reply for a successful execution equest.
590 584 """
591 585 payload = msg['content']['payload']
592 586 for item in payload:
593 587 if not self._process_execute_payload(item):
594 588 warning = 'Warning: received unknown payload of type %s'
595 589 print(warning % repr(item['source']))
596 590
597 591 def _process_execute_payload(self, item):
598 592 """ Process a single payload item from the list of payload items in an
599 593 execution reply. Returns whether the payload was handled.
600 594 """
601 595 # The basic FrontendWidget doesn't handle payloads, as they are a
602 596 # mechanism for going beyond the standard Python interpreter model.
603 597 return False
604 598
605 599 def _show_interpreter_prompt(self):
606 600 """ Shows a prompt for the interpreter.
607 601 """
608 602 self._show_prompt('>>> ')
609 603
610 604 def _show_interpreter_prompt_for_reply(self, msg):
611 605 """ Shows a prompt for the interpreter given an 'execute_reply' message.
612 606 """
613 607 self._show_interpreter_prompt()
614 608
615 609 #------ Signal handlers ----------------------------------------------------
616 610
617 611 def _document_contents_change(self, position, removed, added):
618 612 """ Called whenever the document's content changes. Display a call tip
619 613 if appropriate.
620 614 """
621 615 # Calculate where the cursor should be *after* the change:
622 616 position += added
623 617
624 618 document = self._control.document()
625 619 if position == self._get_cursor().position():
626 620 self._call_tip()
@@ -1,510 +1,511 b''
1 1 """ A FrontendWidget that emulates the interface of the console IPython and
2 2 supports the additional functionality provided by the IPython kernel.
3 3 """
4 4
5 5 #-----------------------------------------------------------------------------
6 6 # Imports
7 7 #-----------------------------------------------------------------------------
8 8
9 9 # Standard library imports
10 10 from collections import namedtuple
11 11 import os.path
12 12 import re
13 13 from subprocess import Popen
14 14 import sys
15 15 from textwrap import dedent
16 16
17 17 # System library imports
18 18 from IPython.external.qt import QtCore, QtGui
19 19
20 20 # Local imports
21 21 from IPython.core.inputsplitter import IPythonInputSplitter, \
22 22 transform_ipy_prompt
23 23 from IPython.core.usage import default_gui_banner
24 24 from IPython.utils.traitlets import Bool, Str, Unicode
25 25 from frontend_widget import FrontendWidget
26 26 import styles
27 27
28 28 #-----------------------------------------------------------------------------
29 29 # Constants
30 30 #-----------------------------------------------------------------------------
31 31
32 32 # Default strings to build and display input and output prompts (and separators
33 33 # in between)
34 34 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
35 35 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
36 36 default_input_sep = '\n'
37 37 default_output_sep = ''
38 38 default_output_sep2 = ''
39 39
40 40 # Base path for most payload sources.
41 41 zmq_shell_source = 'IPython.zmq.zmqshell.ZMQInteractiveShell'
42 42
43 43 if sys.platform.startswith('win'):
44 44 default_editor = 'notepad'
45 45 else:
46 46 default_editor = ''
47 47
48 48 #-----------------------------------------------------------------------------
49 49 # IPythonWidget class
50 50 #-----------------------------------------------------------------------------
51 51
52 52 class IPythonWidget(FrontendWidget):
53 53 """ A FrontendWidget for an IPython kernel.
54 54 """
55 55
56 56 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
57 57 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
58 58 # settings.
59 59 custom_edit = Bool(False)
60 60 custom_edit_requested = QtCore.Signal(object, object)
61 61
62 62 editor = Unicode(default_editor, config=True,
63 63 help="""
64 64 A command for invoking a system text editor. If the string contains a
65 {filename} format specifier, it will be used. Otherwise, the filename will
66 be appended to the end the command.
65 {filename} format specifier, it will be used. Otherwise, the filename
66 will be appended to the end the command.
67 67 """)
68 68
69 69 editor_line = Unicode(config=True,
70 70 help="""
71 71 The editor command to use when a specific line number is requested. The
72 72 string should contain two format specifiers: {line} and {filename}. If
73 this parameter is not specified, the line number option to the %edit magic
74 will be ignored.
73 this parameter is not specified, the line number option to the %edit
74 magic will be ignored.
75 75 """)
76 76
77 77 style_sheet = Unicode(config=True,
78 78 help="""
79 79 A CSS stylesheet. The stylesheet can contain classes for:
80 80 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
81 81 2. Pygments: .c, .k, .o, etc. (see PygmentsHighlighter)
82 82 3. IPython: .error, .in-prompt, .out-prompt, etc
83 83 """)
84 84
85 85
86 86 syntax_style = Str(config=True,
87 87 help="""
88 If not empty, use this Pygments style for syntax highlighting. Otherwise,
89 the style sheet is queried for Pygments style information.
88 If not empty, use this Pygments style for syntax highlighting.
89 Otherwise, the style sheet is queried for Pygments style
90 information.
90 91 """)
91 92
92 93 # Prompts.
93 94 in_prompt = Str(default_in_prompt, config=True)
94 95 out_prompt = Str(default_out_prompt, config=True)
95 96 input_sep = Str(default_input_sep, config=True)
96 97 output_sep = Str(default_output_sep, config=True)
97 98 output_sep2 = Str(default_output_sep2, config=True)
98 99
99 100 # FrontendWidget protected class variables.
100 101 _input_splitter_class = IPythonInputSplitter
101 102
102 103 # IPythonWidget protected class variables.
103 104 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
104 105 _payload_source_edit = zmq_shell_source + '.edit_magic'
105 106 _payload_source_exit = zmq_shell_source + '.ask_exit'
106 107 _payload_source_next_input = zmq_shell_source + '.set_next_input'
107 108 _payload_source_page = 'IPython.zmq.page.page'
108 109
109 110 #---------------------------------------------------------------------------
110 111 # 'object' interface
111 112 #---------------------------------------------------------------------------
112 113
113 114 def __init__(self, *args, **kw):
114 115 super(IPythonWidget, self).__init__(*args, **kw)
115 116
116 117 # IPythonWidget protected variables.
117 118 self._payload_handlers = {
118 119 self._payload_source_edit : self._handle_payload_edit,
119 120 self._payload_source_exit : self._handle_payload_exit,
120 121 self._payload_source_page : self._handle_payload_page,
121 122 self._payload_source_next_input : self._handle_payload_next_input }
122 123 self._previous_prompt_obj = None
123 124 self._keep_kernel_on_exit = None
124 125
125 126 # Initialize widget styling.
126 127 if self.style_sheet:
127 128 self._style_sheet_changed()
128 129 self._syntax_style_changed()
129 130 else:
130 131 self.set_default_style()
131 132
132 133 #---------------------------------------------------------------------------
133 134 # 'BaseFrontendMixin' abstract interface
134 135 #---------------------------------------------------------------------------
135 136
136 137 def _handle_complete_reply(self, rep):
137 138 """ Reimplemented to support IPython's improved completion machinery.
138 139 """
139 140 cursor = self._get_cursor()
140 141 info = self._request_info.get('complete')
141 142 if info and info.id == rep['parent_header']['msg_id'] and \
142 143 info.pos == cursor.position():
143 144 matches = rep['content']['matches']
144 145 text = rep['content']['matched_text']
145 146 offset = len(text)
146 147
147 148 # Clean up matches with period and path separators if the matched
148 149 # text has not been transformed. This is done by truncating all
149 150 # but the last component and then suitably decreasing the offset
150 151 # between the current cursor position and the start of completion.
151 152 if len(matches) > 1 and matches[0][:offset] == text:
152 153 parts = re.split(r'[./\\]', text)
153 154 sep_count = len(parts) - 1
154 155 if sep_count:
155 156 chop_length = sum(map(len, parts[:sep_count])) + sep_count
156 157 matches = [ match[chop_length:] for match in matches ]
157 158 offset -= chop_length
158 159
159 160 # Move the cursor to the start of the match and complete.
160 161 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
161 162 self._complete_with_items(cursor, matches)
162 163
163 164 def _handle_execute_reply(self, msg):
164 165 """ Reimplemented to support prompt requests.
165 166 """
166 167 info = self._request_info.get('execute')
167 168 if info and info.id == msg['parent_header']['msg_id']:
168 169 if info.kind == 'prompt':
169 170 number = msg['content']['execution_count'] + 1
170 171 self._show_interpreter_prompt(number)
171 172 else:
172 173 super(IPythonWidget, self)._handle_execute_reply(msg)
173 174
174 175 def _handle_history_reply(self, msg):
175 176 """ Implemented to handle history tail replies, which are only supported
176 177 by the IPython kernel.
177 178 """
178 179 history_items = msg['content']['history']
179 180 items = [ line.rstrip() for _, _, line in history_items ]
180 181 self._set_history(items)
181 182
182 183 def _handle_pyout(self, msg):
183 184 """ Reimplemented for IPython-style "display hook".
184 185 """
185 186 if not self._hidden and self._is_from_this_session(msg):
186 187 content = msg['content']
187 188 prompt_number = content['execution_count']
188 189 data = content['data']
189 190 if data.has_key('text/html'):
190 self._append_plain_text(self.output_sep)
191 self._append_html(self._make_out_prompt(prompt_number))
191 self._append_plain_text(self.output_sep, True)
192 self._append_html(self._make_out_prompt(prompt_number), True)
192 193 html = data['text/html']
193 self._append_plain_text('\n')
194 self._append_html(html + self.output_sep2)
194 self._append_plain_text('\n', True)
195 self._append_html(html + self.output_sep2, True)
195 196 elif data.has_key('text/plain'):
196 self._append_plain_text(self.output_sep)
197 self._append_html(self._make_out_prompt(prompt_number))
197 self._append_plain_text(self.output_sep, True)
198 self._append_html(self._make_out_prompt(prompt_number), True)
198 199 text = data['text/plain']
199 200 # If the repr is multiline, make sure we start on a new line,
200 201 # so that its lines are aligned.
201 202 if "\n" in text and not self.output_sep.endswith("\n"):
202 self._append_plain_text('\n')
203 self._append_plain_text(text + self.output_sep2)
203 self._append_plain_text('\n', True)
204 self._append_plain_text(text + self.output_sep2, True)
204 205
205 206 def _handle_display_data(self, msg):
206 207 """ The base handler for the ``display_data`` message.
207 208 """
208 209 # For now, we don't display data from other frontends, but we
209 210 # eventually will as this allows all frontends to monitor the display
210 211 # data. But we need to figure out how to handle this in the GUI.
211 212 if not self._hidden and self._is_from_this_session(msg):
212 213 source = msg['content']['source']
213 214 data = msg['content']['data']
214 215 metadata = msg['content']['metadata']
215 216 # In the regular IPythonWidget, we simply print the plain text
216 217 # representation.
217 218 if data.has_key('text/html'):
218 219 html = data['text/html']
219 self._append_html(html)
220 self._append_html(html, before_prompt=True)
220 221 elif data.has_key('text/plain'):
221 222 text = data['text/plain']
222 self._append_plain_text(text)
223 self._append_plain_text(text, before_prompt=True)
223 224 # This newline seems to be needed for text and html output.
224 self._append_plain_text(u'\n')
225 self._append_plain_text(u'\n', before_prompt=True)
225 226
226 227 def _started_channels(self):
227 228 """ Reimplemented to make a history request.
228 229 """
229 230 super(IPythonWidget, self)._started_channels()
230 self.kernel_manager.shell_channel.history(hist_access_type='tail', n=1000)
231
231 self.kernel_manager.shell_channel.history(hist_access_type='tail',
232 n=1000)
232 233 #---------------------------------------------------------------------------
233 234 # 'ConsoleWidget' public interface
234 235 #---------------------------------------------------------------------------
235 236
236 237 def copy(self):
237 238 """ Copy the currently selected text to the clipboard, removing prompts
238 239 if possible.
239 240 """
240 241 text = self._control.textCursor().selection().toPlainText()
241 242 if text:
242 243 lines = map(transform_ipy_prompt, text.splitlines())
243 244 text = '\n'.join(lines)
244 245 QtGui.QApplication.clipboard().setText(text)
245 246
246 247 #---------------------------------------------------------------------------
247 248 # 'FrontendWidget' public interface
248 249 #---------------------------------------------------------------------------
249 250
250 251 def execute_file(self, path, hidden=False):
251 252 """ Reimplemented to use the 'run' magic.
252 253 """
253 254 # Use forward slashes on Windows to avoid escaping each separator.
254 255 if sys.platform == 'win32':
255 256 path = os.path.normpath(path).replace('\\', '/')
256 257
257 258 self.execute('%%run %s' % path, hidden=hidden)
258 259
259 260 #---------------------------------------------------------------------------
260 261 # 'FrontendWidget' protected interface
261 262 #---------------------------------------------------------------------------
262 263
263 264 def _complete(self):
264 265 """ Reimplemented to support IPython's improved completion machinery.
265 266 """
266 267 # We let the kernel split the input line, so we *always* send an empty
267 268 # text field. Readline-based frontends do get a real text field which
268 269 # they can use.
269 270 text = ''
270 271
271 272 # Send the completion request to the kernel
272 273 msg_id = self.kernel_manager.shell_channel.complete(
273 274 text, # text
274 275 self._get_input_buffer_cursor_line(), # line
275 276 self._get_input_buffer_cursor_column(), # cursor_pos
276 277 self.input_buffer) # block
277 278 pos = self._get_cursor().position()
278 279 info = self._CompletionRequest(msg_id, pos)
279 280 self._request_info['complete'] = info
280 281
281 282 def _get_banner(self):
282 283 """ Reimplemented to return IPython's default banner.
283 284 """
284 285 return default_gui_banner
285 286
286 287 def _process_execute_error(self, msg):
287 288 """ Reimplemented for IPython-style traceback formatting.
288 289 """
289 290 content = msg['content']
290 291 traceback = '\n'.join(content['traceback']) + '\n'
291 292 if False:
292 293 # FIXME: For now, tracebacks come as plain text, so we can't use
293 294 # the html renderer yet. Once we refactor ultratb to produce
294 295 # properly styled tracebacks, this branch should be the default
295 296 traceback = traceback.replace(' ', '&nbsp;')
296 297 traceback = traceback.replace('\n', '<br/>')
297 298
298 299 ename = content['ename']
299 300 ename_styled = '<span class="error">%s</span>' % ename
300 301 traceback = traceback.replace(ename, ename_styled)
301 302
302 303 self._append_html(traceback)
303 304 else:
304 305 # This is the fallback for now, using plain text with ansi escapes
305 306 self._append_plain_text(traceback)
306 307
307 308 def _process_execute_payload(self, item):
308 309 """ Reimplemented to dispatch payloads to handler methods.
309 310 """
310 311 handler = self._payload_handlers.get(item['source'])
311 312 if handler is None:
312 313 # We have no handler for this type of payload, simply ignore it
313 314 return False
314 315 else:
315 316 handler(item)
316 317 return True
317 318
318 319 def _show_interpreter_prompt(self, number=None):
319 320 """ Reimplemented for IPython-style prompts.
320 321 """
321 322 # If a number was not specified, make a prompt number request.
322 323 if number is None:
323 324 msg_id = self.kernel_manager.shell_channel.execute('', silent=True)
324 325 info = self._ExecutionRequest(msg_id, 'prompt')
325 326 self._request_info['execute'] = info
326 327 return
327 328
328 329 # Show a new prompt and save information about it so that it can be
329 330 # updated later if the prompt number turns out to be wrong.
330 331 self._prompt_sep = self.input_sep
331 332 self._show_prompt(self._make_in_prompt(number), html=True)
332 333 block = self._control.document().lastBlock()
333 334 length = len(self._prompt)
334 335 self._previous_prompt_obj = self._PromptBlock(block, length, number)
335 336
336 337 # Update continuation prompt to reflect (possibly) new prompt length.
337 338 self._set_continuation_prompt(
338 339 self._make_continuation_prompt(self._prompt), html=True)
339 340
340 341 def _show_interpreter_prompt_for_reply(self, msg):
341 342 """ Reimplemented for IPython-style prompts.
342 343 """
343 344 # Update the old prompt number if necessary.
344 345 content = msg['content']
345 346 previous_prompt_number = content['execution_count']
346 347 if self._previous_prompt_obj and \
347 348 self._previous_prompt_obj.number != previous_prompt_number:
348 349 block = self._previous_prompt_obj.block
349 350
350 351 # Make sure the prompt block has not been erased.
351 352 if block.isValid() and block.text():
352 353
353 354 # Remove the old prompt and insert a new prompt.
354 355 cursor = QtGui.QTextCursor(block)
355 356 cursor.movePosition(QtGui.QTextCursor.Right,
356 357 QtGui.QTextCursor.KeepAnchor,
357 358 self._previous_prompt_obj.length)
358 359 prompt = self._make_in_prompt(previous_prompt_number)
359 360 self._prompt = self._insert_html_fetching_plain_text(
360 361 cursor, prompt)
361 362
362 363 # When the HTML is inserted, Qt blows away the syntax
363 364 # highlighting for the line, so we need to rehighlight it.
364 365 self._highlighter.rehighlightBlock(cursor.block())
365 366
366 367 self._previous_prompt_obj = None
367 368
368 369 # Show a new prompt with the kernel's estimated prompt number.
369 370 self._show_interpreter_prompt(previous_prompt_number + 1)
370 371
371 372 #---------------------------------------------------------------------------
372 373 # 'IPythonWidget' interface
373 374 #---------------------------------------------------------------------------
374 375
375 376 def set_default_style(self, colors='lightbg'):
376 377 """ Sets the widget style to the class defaults.
377 378
378 379 Parameters:
379 380 -----------
380 381 colors : str, optional (default lightbg)
381 382 Whether to use the default IPython light background or dark
382 383 background or B&W style.
383 384 """
384 385 colors = colors.lower()
385 386 if colors=='lightbg':
386 387 self.style_sheet = styles.default_light_style_sheet
387 388 self.syntax_style = styles.default_light_syntax_style
388 389 elif colors=='linux':
389 390 self.style_sheet = styles.default_dark_style_sheet
390 391 self.syntax_style = styles.default_dark_syntax_style
391 392 elif colors=='nocolor':
392 393 self.style_sheet = styles.default_bw_style_sheet
393 394 self.syntax_style = styles.default_bw_syntax_style
394 395 else:
395 396 raise KeyError("No such color scheme: %s"%colors)
396 397
397 398 #---------------------------------------------------------------------------
398 399 # 'IPythonWidget' protected interface
399 400 #---------------------------------------------------------------------------
400 401
401 402 def _edit(self, filename, line=None):
402 403 """ Opens a Python script for editing.
403 404
404 405 Parameters:
405 406 -----------
406 407 filename : str
407 408 A path to a local system file.
408 409
409 410 line : int, optional
410 411 A line of interest in the file.
411 412 """
412 413 if self.custom_edit:
413 414 self.custom_edit_requested.emit(filename, line)
414 415 elif not self.editor:
415 416 self._append_plain_text('No default editor available.\n'
416 'Specify a GUI text editor in the `IPythonWidget.editor` configurable\n'
417 'to enable the %edit magic')
417 'Specify a GUI text editor in the `IPythonWidget.editor` '
418 'configurable to enable the %edit magic')
418 419 else:
419 420 try:
420 421 filename = '"%s"' % filename
421 422 if line and self.editor_line:
422 423 command = self.editor_line.format(filename=filename,
423 424 line=line)
424 425 else:
425 426 try:
426 427 command = self.editor.format()
427 428 except KeyError:
428 429 command = self.editor.format(filename=filename)
429 430 else:
430 431 command += ' ' + filename
431 432 except KeyError:
432 433 self._append_plain_text('Invalid editor command.\n')
433 434 else:
434 435 try:
435 436 Popen(command, shell=True)
436 437 except OSError:
437 438 msg = 'Opening editor with command "%s" failed.\n'
438 439 self._append_plain_text(msg % command)
439 440
440 441 def _make_in_prompt(self, number):
441 442 """ Given a prompt number, returns an HTML In prompt.
442 443 """
443 444 body = self.in_prompt % number
444 445 return '<span class="in-prompt">%s</span>' % body
445 446
446 447 def _make_continuation_prompt(self, prompt):
447 448 """ Given a plain text version of an In prompt, returns an HTML
448 449 continuation prompt.
449 450 """
450 451 end_chars = '...: '
451 452 space_count = len(prompt.lstrip('\n')) - len(end_chars)
452 453 body = '&nbsp;' * space_count + end_chars
453 454 return '<span class="in-prompt">%s</span>' % body
454 455
455 456 def _make_out_prompt(self, number):
456 457 """ Given a prompt number, returns an HTML Out prompt.
457 458 """
458 459 body = self.out_prompt % number
459 460 return '<span class="out-prompt">%s</span>' % body
460 461
461 462 #------ Payload handlers --------------------------------------------------
462 463
463 464 # Payload handlers with a generic interface: each takes the opaque payload
464 465 # dict, unpacks it and calls the underlying functions with the necessary
465 466 # arguments.
466 467
467 468 def _handle_payload_edit(self, item):
468 469 self._edit(item['filename'], item['line_number'])
469 470
470 471 def _handle_payload_exit(self, item):
471 472 self._keep_kernel_on_exit = item['keepkernel']
472 473 self.exit_requested.emit()
473 474
474 475 def _handle_payload_next_input(self, item):
475 476 self.input_buffer = dedent(item['text'].rstrip())
476 477
477 478 def _handle_payload_page(self, item):
478 479 # Since the plain text widget supports only a very small subset of HTML
479 480 # and we have no control over the HTML source, we only page HTML
480 481 # payloads in the rich text widget.
481 482 if item['html'] and self.kind == 'rich':
482 483 self._page(item['html'], html=True)
483 484 else:
484 485 self._page(item['text'], html=False)
485 486
486 487 #------ Trait change handlers --------------------------------------------
487 488
488 489 def _style_sheet_changed(self):
489 490 """ Set the style sheets of the underlying widgets.
490 491 """
491 492 self.setStyleSheet(self.style_sheet)
492 493 self._control.document().setDefaultStyleSheet(self.style_sheet)
493 494 if self._page_control:
494 495 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
495 496
496 497 bg_color = self._control.palette().window().color()
497 498 self._ansi_processor.set_background_color(bg_color)
498 499
499 500
500 501 def _syntax_style_changed(self):
501 502 """ Set the style for the syntax highlighter.
502 503 """
503 504 if self._highlighter is None:
504 505 # ignore premature calls
505 506 return
506 507 if self.syntax_style:
507 508 self._highlighter.set_style(self.syntax_style)
508 509 else:
509 510 self._highlighter.set_style_sheet(self.style_sheet)
510 511
General Comments 0
You need to be logged in to leave comments. Login now