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