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