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