##// END OF EJS Templates
Fixed bug with ConsoleWidget smart paste.
epatters -
Show More
@@ -1,1237 +1,1248 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(QtCore.QString)
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(QString)' 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 219 def clear(self, keep_input=False):
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 223 self._control.clear()
224 224 if keep_input:
225 225 input_buffer = self.input_buffer
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 self._insert_into_buffer(dedent(text))
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 anchor = self._get_end_cursor().position()
481 self._append_html(html)
482 480 cursor = self._get_end_cursor()
483 cursor.setPosition(anchor, QtGui.QTextCursor.KeepAnchor)
484 return str(cursor.selection().toPlainText())
481 return self._insert_html_fetching_plain_text(cursor, html)
485 482
486 483 def _append_plain_text(self, text):
487 484 """ Appends plain text at the end of the console buffer, processing
488 485 ANSI codes if enabled.
489 486 """
490 487 cursor = self._get_end_cursor()
491 488 self._insert_plain_text(cursor, text)
492 489
493 490 def _append_plain_text_keeping_prompt(self, text):
494 491 """ Writes 'text' after the current prompt, then restores the old prompt
495 492 with its old input buffer.
496 493 """
497 494 input_buffer = self.input_buffer
498 495 self._append_plain_text('\n')
499 496 self._prompt_finished()
500 497
501 498 self._append_plain_text(text)
502 499 self._show_prompt()
503 500 self.input_buffer = input_buffer
504 501
505 502 def _complete_with_items(self, cursor, items):
506 503 """ Performs completion with 'items' at the specified cursor location.
507 504 """
508 505 if len(items) == 1:
509 506 cursor.setPosition(self._control.textCursor().position(),
510 507 QtGui.QTextCursor.KeepAnchor)
511 508 cursor.insertText(items[0])
512 509 elif len(items) > 1:
513 510 if self.gui_completion:
514 511 self._completion_widget.show_items(cursor, items)
515 512 else:
516 513 text = self._format_as_columns(items)
517 514 self._append_plain_text_keeping_prompt(text)
518 515
519 516 def _control_key_down(self, modifiers):
520 517 """ Given a KeyboardModifiers flags object, return whether the Control
521 518 key is down (on Mac OS, treat the Command key as a synonym for
522 519 Control).
523 520 """
524 521 down = bool(modifiers & QtCore.Qt.ControlModifier)
525 522
526 523 # Note: on Mac OS, ControlModifier corresponds to the Command key while
527 524 # MetaModifier corresponds to the Control key.
528 525 if sys.platform == 'darwin':
529 526 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
530 527
531 528 return down
532 529
533 530 def _create_control(self, kind):
534 531 """ Creates and connects the underlying text widget.
535 532 """
536 533 if kind == 'plain':
537 534 control = QtGui.QPlainTextEdit()
538 535 elif kind == 'rich':
539 536 control = QtGui.QTextEdit()
540 537 control.setAcceptRichText(False)
541 538 else:
542 539 raise ValueError("Kind %s unknown." % repr(kind))
543 540 control.installEventFilter(self)
544 541 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
545 542 control.customContextMenuRequested.connect(self._show_context_menu)
546 543 control.copyAvailable.connect(self.copy_available)
547 544 control.redoAvailable.connect(self.redo_available)
548 545 control.undoAvailable.connect(self.undo_available)
549 546 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
550 547 return control
551 548
552 549 def _create_page_control(self):
553 550 """ Creates and connects the underlying paging widget.
554 551 """
555 552 control = QtGui.QPlainTextEdit()
556 553 control.installEventFilter(self)
557 554 control.setReadOnly(True)
558 555 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
559 556 return control
560 557
561 558 def _event_filter_console_keypress(self, event):
562 559 """ Filter key events for the underlying text widget to create a
563 560 console-like interface.
564 561 """
565 562 intercepted = False
566 563 cursor = self._control.textCursor()
567 564 position = cursor.position()
568 565 key = event.key()
569 566 ctrl_down = self._control_key_down(event.modifiers())
570 567 alt_down = event.modifiers() & QtCore.Qt.AltModifier
571 568 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
572 569
573 570 if event.matches(QtGui.QKeySequence.Paste):
574 571 # Call our paste instead of the underlying text widget's.
575 572 self.paste()
576 573 intercepted = True
577 574
578 575 elif ctrl_down:
579 576 if key == QtCore.Qt.Key_C:
580 577 intercepted = self._executing and self._execute_interrupt()
581 578
582 579 elif key == QtCore.Qt.Key_K:
583 580 if self._in_buffer(position):
584 581 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
585 582 QtGui.QTextCursor.KeepAnchor)
586 583 cursor.removeSelectedText()
587 584 intercepted = True
588 585
589 586 elif key == QtCore.Qt.Key_X:
590 587 intercepted = True
591 588
592 589 elif key == QtCore.Qt.Key_Y:
593 590 self.paste()
594 591 intercepted = True
595 592
596 593 elif alt_down:
597 594 if key == QtCore.Qt.Key_B:
598 595 self._set_cursor(self._get_word_start_cursor(position))
599 596 intercepted = True
600 597
601 598 elif key == QtCore.Qt.Key_F:
602 599 self._set_cursor(self._get_word_end_cursor(position))
603 600 intercepted = True
604 601
605 602 elif key == QtCore.Qt.Key_Backspace:
606 603 cursor = self._get_word_start_cursor(position)
607 604 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
608 605 cursor.removeSelectedText()
609 606 intercepted = True
610 607
611 608 elif key == QtCore.Qt.Key_D:
612 609 cursor = self._get_word_end_cursor(position)
613 610 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
614 611 cursor.removeSelectedText()
615 612 intercepted = True
616 613
617 614 else:
618 615 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
619 616 if self._reading:
620 617 self._append_plain_text('\n')
621 618 self._reading = False
622 619 if self._reading_callback:
623 620 self._reading_callback()
624 621 elif not self._executing:
625 622 self.execute(interactive=True)
626 623 intercepted = True
627 624
628 625 elif key == QtCore.Qt.Key_Up:
629 626 if self._reading or not self._up_pressed():
630 627 intercepted = True
631 628 else:
632 629 prompt_line = self._get_prompt_cursor().blockNumber()
633 630 intercepted = cursor.blockNumber() <= prompt_line
634 631
635 632 elif key == QtCore.Qt.Key_Down:
636 633 if self._reading or not self._down_pressed():
637 634 intercepted = True
638 635 else:
639 636 end_line = self._get_end_cursor().blockNumber()
640 637 intercepted = cursor.blockNumber() == end_line
641 638
642 639 elif key == QtCore.Qt.Key_Tab:
643 640 if self._reading:
644 641 intercepted = False
645 642 else:
646 643 intercepted = not self._tab_pressed()
647 644
648 645 elif key == QtCore.Qt.Key_Left:
649 646 intercepted = not self._in_buffer(position - 1)
650 647
651 648 elif key == QtCore.Qt.Key_Home:
652 649 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
653 650 start_line = cursor.blockNumber()
654 651 if start_line == self._get_prompt_cursor().blockNumber():
655 652 start_pos = self._prompt_pos
656 653 else:
657 654 start_pos = cursor.position()
658 655 start_pos += len(self._continuation_prompt)
659 656 if shift_down and self._in_buffer(position):
660 657 self._set_selection(position, start_pos)
661 658 else:
662 659 self._set_position(start_pos)
663 660 intercepted = True
664 661
665 662 elif key == QtCore.Qt.Key_Backspace:
666 663
667 664 # Line deletion (remove continuation prompt)
668 665 len_prompt = len(self._continuation_prompt)
669 666 if not self._reading and \
670 667 cursor.columnNumber() == len_prompt and \
671 668 position != self._prompt_pos:
672 669 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
673 670 QtGui.QTextCursor.KeepAnchor)
674 671 cursor.removeSelectedText()
675 672 cursor.deletePreviousChar()
676 673 intercepted = True
677 674
678 675 # Regular backwards deletion
679 676 else:
680 677 anchor = cursor.anchor()
681 678 if anchor == position:
682 679 intercepted = not self._in_buffer(position - 1)
683 680 else:
684 681 intercepted = not self._in_buffer(min(anchor, position))
685 682
686 683 elif key == QtCore.Qt.Key_Delete:
687 684 anchor = cursor.anchor()
688 685 intercepted = not self._in_buffer(min(anchor, position))
689 686
690 687 # Don't move the cursor if control is down to allow copy-paste using
691 688 # the keyboard in any part of the buffer.
692 689 if not ctrl_down:
693 690 self._keep_cursor_in_buffer()
694 691
695 692 return intercepted
696 693
697 694 def _event_filter_page_keypress(self, event):
698 695 """ Filter key events for the paging widget to create console-like
699 696 interface.
700 697 """
701 698 key = event.key()
702 699
703 700 if key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
704 701 if self._splitter:
705 702 self._page_control.hide()
706 703 else:
707 704 self.layout().setCurrentWidget(self._control)
708 705 return True
709 706
710 707 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
711 708 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
712 709 QtCore.Qt.Key_Down,
713 710 QtCore.Qt.NoModifier)
714 711 QtGui.qApp.sendEvent(self._page_control, new_event)
715 712 return True
716 713
717 714 return False
718 715
719 716 def _format_as_columns(self, items, separator=' '):
720 717 """ Transform a list of strings into a single string with columns.
721 718
722 719 Parameters
723 720 ----------
724 721 items : sequence of strings
725 722 The strings to process.
726 723
727 724 separator : str, optional [default is two spaces]
728 725 The string that separates columns.
729 726
730 727 Returns
731 728 -------
732 729 The formatted string.
733 730 """
734 731 # Note: this code is adapted from columnize 0.3.2.
735 732 # See http://code.google.com/p/pycolumnize/
736 733
737 734 width = self._control.viewport().width()
738 735 char_width = QtGui.QFontMetrics(self.font).width(' ')
739 736 displaywidth = max(5, width / char_width)
740 737
741 738 # Some degenerate cases.
742 739 size = len(items)
743 740 if size == 0:
744 741 return '\n'
745 742 elif size == 1:
746 743 return '%s\n' % str(items[0])
747 744
748 745 # Try every row count from 1 upwards
749 746 array_index = lambda nrows, row, col: nrows*col + row
750 747 for nrows in range(1, size):
751 748 ncols = (size + nrows - 1) // nrows
752 749 colwidths = []
753 750 totwidth = -len(separator)
754 751 for col in range(ncols):
755 752 # Get max column width for this column
756 753 colwidth = 0
757 754 for row in range(nrows):
758 755 i = array_index(nrows, row, col)
759 756 if i >= size: break
760 757 x = items[i]
761 758 colwidth = max(colwidth, len(x))
762 759 colwidths.append(colwidth)
763 760 totwidth += colwidth + len(separator)
764 761 if totwidth > displaywidth:
765 762 break
766 763 if totwidth <= displaywidth:
767 764 break
768 765
769 766 # The smallest number of rows computed and the max widths for each
770 767 # column has been obtained. Now we just have to format each of the rows.
771 768 string = ''
772 769 for row in range(nrows):
773 770 texts = []
774 771 for col in range(ncols):
775 772 i = row + nrows*col
776 773 if i >= size:
777 774 texts.append('')
778 775 else:
779 776 texts.append(items[i])
780 777 while texts and not texts[-1]:
781 778 del texts[-1]
782 779 for col in range(len(texts)):
783 780 texts[col] = texts[col].ljust(colwidths[col])
784 781 string += '%s\n' % str(separator.join(texts))
785 782 return string
786 783
787 784 def _get_block_plain_text(self, block):
788 785 """ Given a QTextBlock, return its unformatted text.
789 786 """
790 787 cursor = QtGui.QTextCursor(block)
791 788 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
792 789 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
793 790 QtGui.QTextCursor.KeepAnchor)
794 791 return str(cursor.selection().toPlainText())
795 792
796 793 def _get_cursor(self):
797 794 """ Convenience method that returns a cursor for the current position.
798 795 """
799 796 return self._control.textCursor()
800 797
801 798 def _get_end_cursor(self):
802 799 """ Convenience method that returns a cursor for the last character.
803 800 """
804 801 cursor = self._control.textCursor()
805 802 cursor.movePosition(QtGui.QTextCursor.End)
806 803 return cursor
807 804
808 805 def _get_input_buffer_cursor_line(self):
809 806 """ The text in the line of the input buffer in which the user's cursor
810 807 rests. Returns a string if there is such a line; otherwise, None.
811 808 """
812 809 if self._executing:
813 810 return None
814 811 cursor = self._control.textCursor()
815 812 if cursor.position() >= self._prompt_pos:
816 813 text = self._get_block_plain_text(cursor.block())
817 814 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
818 815 return text[len(self._prompt):]
819 816 else:
820 817 return text[len(self._continuation_prompt):]
821 818 else:
822 819 return None
823 820
824 821 def _get_prompt_cursor(self):
825 822 """ Convenience method that returns a cursor for the prompt position.
826 823 """
827 824 cursor = self._control.textCursor()
828 825 cursor.setPosition(self._prompt_pos)
829 826 return cursor
830 827
831 828 def _get_selection_cursor(self, start, end):
832 829 """ Convenience method that returns a cursor with text selected between
833 830 the positions 'start' and 'end'.
834 831 """
835 832 cursor = self._control.textCursor()
836 833 cursor.setPosition(start)
837 834 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
838 835 return cursor
839 836
840 837 def _get_word_start_cursor(self, position):
841 838 """ Find the start of the word to the left the given position. If a
842 839 sequence of non-word characters precedes the first word, skip over
843 840 them. (This emulates the behavior of bash, emacs, etc.)
844 841 """
845 842 document = self._control.document()
846 843 position -= 1
847 844 while position >= self._prompt_pos and \
848 845 not document.characterAt(position).isLetterOrNumber():
849 846 position -= 1
850 847 while position >= self._prompt_pos and \
851 848 document.characterAt(position).isLetterOrNumber():
852 849 position -= 1
853 850 cursor = self._control.textCursor()
854 851 cursor.setPosition(position + 1)
855 852 return cursor
856 853
857 854 def _get_word_end_cursor(self, position):
858 855 """ Find the end of the word to the right the given position. If a
859 856 sequence of non-word characters precedes the first word, skip over
860 857 them. (This emulates the behavior of bash, emacs, etc.)
861 858 """
862 859 document = self._control.document()
863 860 end = self._get_end_cursor().position()
864 861 while position < end and \
865 862 not document.characterAt(position).isLetterOrNumber():
866 863 position += 1
867 864 while position < end and \
868 865 document.characterAt(position).isLetterOrNumber():
869 866 position += 1
870 867 cursor = self._control.textCursor()
871 868 cursor.setPosition(position)
872 869 return cursor
873 870
874 871 def _insert_html(self, cursor, html):
875 """ Insert HTML using the specified cursor in such a way that future
872 """ Inserts HTML using the specified cursor in such a way that future
876 873 formatting is unaffected.
877 874 """
878 875 cursor.beginEditBlock()
879 876 cursor.insertHtml(html)
880 877
881 878 # After inserting HTML, the text document "remembers" it's in "html
882 879 # mode", which means that subsequent calls adding plain text will result
883 880 # in unwanted formatting, lost tab characters, etc. The following code
884 881 # hacks around this behavior, which I consider to be a bug in Qt.
885 882 cursor.movePosition(QtGui.QTextCursor.Left,
886 883 QtGui.QTextCursor.KeepAnchor)
887 884 if cursor.selection().toPlainText() == ' ':
888 885 cursor.removeSelectedText()
889 886 cursor.movePosition(QtGui.QTextCursor.Right)
890 887 cursor.insertText(' ', QtGui.QTextCharFormat())
891 888 cursor.endEditBlock()
892 889
890 def _insert_html_fetching_plain_text(self, cursor, html):
891 """ Inserts HTML using the specified cursor, then returns its plain text
892 version.
893 """
894 start = cursor.position()
895 self._insert_html(cursor, html)
896 end = cursor.position()
897 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
898 text = str(cursor.selection().toPlainText())
899 cursor.setPosition(end)
900 return text
901
893 902 def _insert_plain_text(self, cursor, text):
894 903 """ Inserts plain text using the specified cursor, processing ANSI codes
895 904 if enabled.
896 905 """
897 906 cursor.beginEditBlock()
898 907 if self.ansi_codes:
899 908 for substring in self._ansi_processor.split_string(text):
900 909 for action in self._ansi_processor.actions:
901 910 if action.kind == 'erase' and action.area == 'screen':
902 911 cursor.select(QtGui.QTextCursor.Document)
903 912 cursor.removeSelectedText()
904 913 format = self._ansi_processor.get_format()
905 914 cursor.insertText(substring, format)
906 915 else:
907 916 cursor.insertText(text)
908 917 cursor.endEditBlock()
909 918
910 def _insert_into_buffer(self, text):
919 def _insert_plain_text_into_buffer(self, text):
911 920 """ Inserts text into the input buffer at the current cursor position,
912 921 ensuring that continuation prompts are inserted as necessary.
913 922 """
914 923 lines = str(text).splitlines(True)
915 924 if lines:
916 925 self._keep_cursor_in_buffer()
917 926 cursor = self._control.textCursor()
918 927 cursor.beginEditBlock()
919 928 cursor.insertText(lines[0])
920 929 for line in lines[1:]:
921 930 if self._continuation_prompt_html is None:
922 931 cursor.insertText(self._continuation_prompt)
923 932 else:
924 self._insert_html(cursor, self._continuation_prompt_html)
933 self._continuation_prompt = \
934 self._insert_html_fetching_plain_text(
935 cursor, self._continuation_prompt_html)
925 936 cursor.insertText(line)
926 937 cursor.endEditBlock()
927 938 self._control.setTextCursor(cursor)
928 939
929 940 def _in_buffer(self, position):
930 941 """ Returns whether the given position is inside the editing region.
931 942 """
932 943 cursor = self._control.textCursor()
933 944 cursor.setPosition(position)
934 945 line = cursor.blockNumber()
935 946 prompt_line = self._get_prompt_cursor().blockNumber()
936 947 if line == prompt_line:
937 948 return position >= self._prompt_pos
938 949 elif line > prompt_line:
939 950 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
940 951 prompt_pos = cursor.position() + len(self._continuation_prompt)
941 952 return position >= prompt_pos
942 953 return False
943 954
944 955 def _keep_cursor_in_buffer(self):
945 956 """ Ensures that the cursor is inside the editing region. Returns
946 957 whether the cursor was moved.
947 958 """
948 959 cursor = self._control.textCursor()
949 960 if self._in_buffer(cursor.position()):
950 961 return False
951 962 else:
952 963 cursor.movePosition(QtGui.QTextCursor.End)
953 964 self._control.setTextCursor(cursor)
954 965 return True
955 966
956 967 def _page(self, text):
957 968 """ Displays text using the pager.
958 969 """
959 970 if self._page_style == 'custom':
960 971 self.custom_page_requested.emit(text)
961 972 elif self._page_style == 'none':
962 973 self._append_plain_text(text)
963 974 else:
964 975 self._page_control.clear()
965 976 cursor = self._page_control.textCursor()
966 977 self._insert_plain_text(cursor, text)
967 978 self._page_control.moveCursor(QtGui.QTextCursor.Start)
968 979
969 980 self._page_control.viewport().resize(self._control.size())
970 981 if self._splitter:
971 982 self._page_control.show()
972 983 self._page_control.setFocus()
973 984 else:
974 985 self.layout().setCurrentWidget(self._page_control)
975 986
976 987 def _prompt_started(self):
977 988 """ Called immediately after a new prompt is displayed.
978 989 """
979 990 # Temporarily disable the maximum block count to permit undo/redo and
980 991 # to ensure that the prompt position does not change due to truncation.
981 992 self._control.document().setMaximumBlockCount(0)
982 993 self._control.setUndoRedoEnabled(True)
983 994
984 995 self._control.setReadOnly(False)
985 996 self._control.moveCursor(QtGui.QTextCursor.End)
986 997
987 998 self._executing = False
988 999 self._prompt_started_hook()
989 1000
990 1001 def _prompt_finished(self):
991 1002 """ Called immediately after a prompt is finished, i.e. when some input
992 1003 will be processed and a new prompt displayed.
993 1004 """
994 1005 self._control.setUndoRedoEnabled(False)
995 1006 self._control.setReadOnly(True)
996 1007 self._prompt_finished_hook()
997 1008
998 1009 def _readline(self, prompt='', callback=None):
999 1010 """ Reads one line of input from the user.
1000 1011
1001 1012 Parameters
1002 1013 ----------
1003 1014 prompt : str, optional
1004 1015 The prompt to print before reading the line.
1005 1016
1006 1017 callback : callable, optional
1007 1018 A callback to execute with the read line. If not specified, input is
1008 1019 read *synchronously* and this method does not return until it has
1009 1020 been read.
1010 1021
1011 1022 Returns
1012 1023 -------
1013 1024 If a callback is specified, returns nothing. Otherwise, returns the
1014 1025 input string with the trailing newline stripped.
1015 1026 """
1016 1027 if self._reading:
1017 1028 raise RuntimeError('Cannot read a line. Widget is already reading.')
1018 1029
1019 1030 if not callback and not self.isVisible():
1020 1031 # If the user cannot see the widget, this function cannot return.
1021 1032 raise RuntimeError('Cannot synchronously read a line if the widget'
1022 1033 'is not visible!')
1023 1034
1024 1035 self._reading = True
1025 1036 self._show_prompt(prompt, newline=False)
1026 1037
1027 1038 if callback is None:
1028 1039 self._reading_callback = None
1029 1040 while self._reading:
1030 1041 QtCore.QCoreApplication.processEvents()
1031 1042 return self.input_buffer.rstrip('\n')
1032 1043
1033 1044 else:
1034 1045 self._reading_callback = lambda: \
1035 1046 callback(self.input_buffer.rstrip('\n'))
1036 1047
1037 1048 def _reset(self):
1038 1049 """ Clears the console and resets internal state variables.
1039 1050 """
1040 1051 self._control.clear()
1041 1052 self._executing = self._reading = False
1042 1053
1043 1054 def _set_continuation_prompt(self, prompt, html=False):
1044 1055 """ Sets the continuation prompt.
1045 1056
1046 1057 Parameters
1047 1058 ----------
1048 1059 prompt : str
1049 1060 The prompt to show when more input is needed.
1050 1061
1051 1062 html : bool, optional (default False)
1052 1063 If set, the prompt will be inserted as formatted HTML. Otherwise,
1053 1064 the prompt will be treated as plain text, though ANSI color codes
1054 1065 will be handled.
1055 1066 """
1056 1067 if html:
1057 1068 self._continuation_prompt_html = prompt
1058 1069 else:
1059 1070 self._continuation_prompt = prompt
1060 1071 self._continuation_prompt_html = None
1061 1072
1062 1073 def _set_cursor(self, cursor):
1063 1074 """ Convenience method to set the current cursor.
1064 1075 """
1065 1076 self._control.setTextCursor(cursor)
1066 1077
1067 1078 def _set_position(self, position):
1068 1079 """ Convenience method to set the position of the cursor.
1069 1080 """
1070 1081 cursor = self._control.textCursor()
1071 1082 cursor.setPosition(position)
1072 1083 self._control.setTextCursor(cursor)
1073 1084
1074 1085 def _set_selection(self, start, end):
1075 1086 """ Convenience method to set the current selected text.
1076 1087 """
1077 1088 self._control.setTextCursor(self._get_selection_cursor(start, end))
1078 1089
1079 1090 def _show_context_menu(self, pos):
1080 1091 """ Shows a context menu at the given QPoint (in widget coordinates).
1081 1092 """
1082 1093 menu = QtGui.QMenu()
1083 1094
1084 1095 copy_action = menu.addAction('Copy', self.copy)
1085 1096 copy_action.setEnabled(self._get_cursor().hasSelection())
1086 1097 copy_action.setShortcut(QtGui.QKeySequence.Copy)
1087 1098
1088 1099 paste_action = menu.addAction('Paste', self.paste)
1089 1100 paste_action.setEnabled(self.can_paste())
1090 1101 paste_action.setShortcut(QtGui.QKeySequence.Paste)
1091 1102
1092 1103 menu.addSeparator()
1093 1104 menu.addAction('Select All', self.select_all)
1094 1105
1095 1106 menu.exec_(self._control.mapToGlobal(pos))
1096 1107
1097 1108 def _show_prompt(self, prompt=None, html=False, newline=True):
1098 1109 """ Writes a new prompt at the end of the buffer.
1099 1110
1100 1111 Parameters
1101 1112 ----------
1102 1113 prompt : str, optional
1103 1114 The prompt to show. If not specified, the previous prompt is used.
1104 1115
1105 1116 html : bool, optional (default False)
1106 1117 Only relevant when a prompt is specified. If set, the prompt will
1107 1118 be inserted as formatted HTML. Otherwise, the prompt will be treated
1108 1119 as plain text, though ANSI color codes will be handled.
1109 1120
1110 1121 newline : bool, optional (default True)
1111 1122 If set, a new line will be written before showing the prompt if
1112 1123 there is not already a newline at the end of the buffer.
1113 1124 """
1114 1125 # Insert a preliminary newline, if necessary.
1115 1126 if newline:
1116 1127 cursor = self._get_end_cursor()
1117 1128 if cursor.position() > 0:
1118 1129 cursor.movePosition(QtGui.QTextCursor.Left,
1119 1130 QtGui.QTextCursor.KeepAnchor)
1120 1131 if str(cursor.selection().toPlainText()) != '\n':
1121 1132 self._append_plain_text('\n')
1122 1133
1123 1134 # Write the prompt.
1124 1135 if prompt is None:
1125 1136 if self._prompt_html is None:
1126 1137 self._append_plain_text(self._prompt)
1127 1138 else:
1128 1139 self._append_html(self._prompt_html)
1129 1140 else:
1130 1141 if html:
1131 1142 self._prompt = self._append_html_fetching_plain_text(prompt)
1132 1143 self._prompt_html = prompt
1133 1144 else:
1134 1145 self._append_plain_text(prompt)
1135 1146 self._prompt = prompt
1136 1147 self._prompt_html = None
1137 1148
1138 1149 self._prompt_pos = self._get_end_cursor().position()
1139 1150 self._prompt_started()
1140 1151
1141 1152 def _show_continuation_prompt(self):
1142 1153 """ Writes a new continuation prompt at the end of the buffer.
1143 1154 """
1144 1155 if self._continuation_prompt_html is None:
1145 1156 self._append_plain_text(self._continuation_prompt)
1146 1157 else:
1147 1158 self._continuation_prompt = self._append_html_fetching_plain_text(
1148 1159 self._continuation_prompt_html)
1149 1160
1150 1161 self._prompt_started()
1151 1162
1152 1163
1153 1164 class HistoryConsoleWidget(ConsoleWidget):
1154 1165 """ A ConsoleWidget that keeps a history of the commands that have been
1155 1166 executed.
1156 1167 """
1157 1168
1158 1169 #---------------------------------------------------------------------------
1159 1170 # 'object' interface
1160 1171 #---------------------------------------------------------------------------
1161 1172
1162 1173 def __init__(self, *args, **kw):
1163 1174 super(HistoryConsoleWidget, self).__init__(*args, **kw)
1164 1175 self._history = []
1165 1176 self._history_index = 0
1166 1177
1167 1178 #---------------------------------------------------------------------------
1168 1179 # 'ConsoleWidget' public interface
1169 1180 #---------------------------------------------------------------------------
1170 1181
1171 1182 def execute(self, source=None, hidden=False, interactive=False):
1172 1183 """ Reimplemented to the store history.
1173 1184 """
1174 1185 if not hidden:
1175 1186 history = self.input_buffer if source is None else source
1176 1187
1177 1188 executed = super(HistoryConsoleWidget, self).execute(
1178 1189 source, hidden, interactive)
1179 1190
1180 1191 if executed and not hidden:
1181 1192 self._history.append(history.rstrip())
1182 1193 self._history_index = len(self._history)
1183 1194
1184 1195 return executed
1185 1196
1186 1197 #---------------------------------------------------------------------------
1187 1198 # 'ConsoleWidget' abstract interface
1188 1199 #---------------------------------------------------------------------------
1189 1200
1190 1201 def _up_pressed(self):
1191 1202 """ Called when the up key is pressed. Returns whether to continue
1192 1203 processing the event.
1193 1204 """
1194 1205 prompt_cursor = self._get_prompt_cursor()
1195 1206 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
1196 1207 self.history_previous()
1197 1208
1198 1209 # Go to the first line of prompt for seemless history scrolling.
1199 1210 cursor = self._get_prompt_cursor()
1200 1211 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
1201 1212 self._set_cursor(cursor)
1202 1213
1203 1214 return False
1204 1215 return True
1205 1216
1206 1217 def _down_pressed(self):
1207 1218 """ Called when the down key is pressed. Returns whether to continue
1208 1219 processing the event.
1209 1220 """
1210 1221 end_cursor = self._get_end_cursor()
1211 1222 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
1212 1223 self.history_next()
1213 1224 return False
1214 1225 return True
1215 1226
1216 1227 #---------------------------------------------------------------------------
1217 1228 # 'HistoryConsoleWidget' interface
1218 1229 #---------------------------------------------------------------------------
1219 1230
1220 1231 def history_previous(self):
1221 1232 """ If possible, set the input buffer to the previous item in the
1222 1233 history.
1223 1234 """
1224 1235 if self._history_index > 0:
1225 1236 self._history_index -= 1
1226 1237 self.input_buffer = self._history[self._history_index]
1227 1238
1228 1239 def history_next(self):
1229 1240 """ Set the input buffer to the next item in the history, or a blank
1230 1241 line if there is no subsequent item.
1231 1242 """
1232 1243 if self._history_index < len(self._history):
1233 1244 self._history_index += 1
1234 1245 if self._history_index < len(self._history):
1235 1246 self.input_buffer = self._history[self._history_index]
1236 1247 else:
1237 1248 self.input_buffer = ''
@@ -1,344 +1,349 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 setFormat(self, start, count, format):
58 58 """ Reimplemented to highlight selectively.
59 59 """
60 60 start += self._current_offset
61 61 PygmentsHighlighter.setFormat(self, start, count, format)
62 62
63 63
64 64 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
65 65 """ A Qt frontend for a generic Python kernel.
66 66 """
67 67
68 68 # Emitted when an 'execute_reply' has been received from the kernel and
69 69 # processed by the FrontendWidget.
70 70 executed = QtCore.pyqtSignal(object)
71 71
72 72 #---------------------------------------------------------------------------
73 73 # 'object' interface
74 74 #---------------------------------------------------------------------------
75 75
76 76 def __init__(self, *args, **kw):
77 77 super(FrontendWidget, self).__init__(*args, **kw)
78 78
79 79 # FrontendWidget protected variables.
80 80 self._call_tip_widget = CallTipWidget(self._control)
81 81 self._completion_lexer = CompletionLexer(PythonLexer())
82 82 self._hidden = True
83 83 self._highlighter = FrontendHighlighter(self)
84 84 self._input_splitter = InputSplitter(input_mode='replace')
85 85 self._kernel_manager = None
86 86
87 87 # Configure the ConsoleWidget.
88 88 self.tab_width = 4
89 89 self._set_continuation_prompt('... ')
90 90
91 91 # Connect signal handlers.
92 92 document = self._control.document()
93 93 document.contentsChange.connect(self._document_contents_change)
94 94
95 95 #---------------------------------------------------------------------------
96 96 # 'ConsoleWidget' abstract interface
97 97 #---------------------------------------------------------------------------
98 98
99 99 def _is_complete(self, source, interactive):
100 100 """ Returns whether 'source' can be completely processed and a new
101 101 prompt created. When triggered by an Enter/Return key press,
102 102 'interactive' is True; otherwise, it is False.
103 103 """
104 104 complete = self._input_splitter.push(source.expandtabs(4))
105 105 if interactive:
106 106 complete = not self._input_splitter.push_accepts_more()
107 107 return complete
108 108
109 109 def _execute(self, source, hidden):
110 110 """ Execute 'source'. If 'hidden', do not show any output.
111 111 """
112 112 self.kernel_manager.xreq_channel.execute(source)
113 113 self._hidden = hidden
114 114
115 115 def _execute_interrupt(self):
116 116 """ Attempts to stop execution. Returns whether this method has an
117 117 implementation.
118 118 """
119 119 self._interrupt_kernel()
120 120 return True
121 121
122 122 def _prompt_started_hook(self):
123 123 """ Called immediately after a new prompt is displayed.
124 124 """
125 125 if not self._reading:
126 126 self._highlighter.highlighting_on = True
127 127
128 # Auto-indent if this is a continuation prompt.
129 if self._get_prompt_cursor().blockNumber() != \
130 self._get_end_cursor().blockNumber():
131 spaces = self._input_splitter.indent_spaces
132 self._append_plain_text('\t' * (spaces / self.tab_width))
133 self._append_plain_text(' ' * (spaces % self.tab_width))
134
135 128 def _prompt_finished_hook(self):
136 129 """ Called immediately after a prompt is finished, i.e. when some input
137 130 will be processed and a new prompt displayed.
138 131 """
139 132 if not self._reading:
140 133 self._highlighter.highlighting_on = False
141 134
142 135 def _tab_pressed(self):
143 136 """ Called when the tab key is pressed. Returns whether to continue
144 137 processing the event.
145 138 """
146 139 self._keep_cursor_in_buffer()
147 140 cursor = self._get_cursor()
148 141 return not self._complete()
149 142
150 143 #---------------------------------------------------------------------------
144 # 'ConsoleWidget' protected interface
145 #---------------------------------------------------------------------------
146
147 def _show_continuation_prompt(self):
148 """ Reimplemented for auto-indentation.
149 """
150 super(FrontendWidget, self)._show_continuation_prompt()
151 spaces = self._input_splitter.indent_spaces
152 self._append_plain_text('\t' * (spaces / self.tab_width))
153 self._append_plain_text(' ' * (spaces % self.tab_width))
154
155 #---------------------------------------------------------------------------
151 156 # 'BaseFrontendMixin' abstract interface
152 157 #---------------------------------------------------------------------------
153 158
154 159 def _handle_complete_reply(self, rep):
155 160 """ Handle replies for tab completion.
156 161 """
157 162 cursor = self._get_cursor()
158 163 if rep['parent_header']['msg_id'] == self._complete_id and \
159 164 cursor.position() == self._complete_pos:
160 165 text = '.'.join(self._get_context())
161 166 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
162 167 self._complete_with_items(cursor, rep['content']['matches'])
163 168
164 169 def _handle_execute_reply(self, msg):
165 170 """ Handles replies for code execution.
166 171 """
167 172 if not self._hidden:
168 173 # Make sure that all output from the SUB channel has been processed
169 174 # before writing a new prompt.
170 175 self.kernel_manager.sub_channel.flush()
171 176
172 177 content = msg['content']
173 178 status = content['status']
174 179 if status == 'ok':
175 180 self._process_execute_ok(msg)
176 181 elif status == 'error':
177 182 self._process_execute_error(msg)
178 183 elif status == 'abort':
179 184 self._process_execute_abort(msg)
180 185
181 186 self._hidden = True
182 187 self._show_interpreter_prompt()
183 188 self.executed.emit(msg)
184 189
185 190 def _handle_input_request(self, msg):
186 191 """ Handle requests for raw_input.
187 192 """
188 193 if self._hidden:
189 194 raise RuntimeError('Request for raw input during hidden execution.')
190 195
191 196 # Make sure that all output from the SUB channel has been processed
192 197 # before entering readline mode.
193 198 self.kernel_manager.sub_channel.flush()
194 199
195 200 def callback(line):
196 201 self.kernel_manager.rep_channel.input(line)
197 202 self._readline(msg['content']['prompt'], callback=callback)
198 203
199 204 def _handle_object_info_reply(self, rep):
200 205 """ Handle replies for call tips.
201 206 """
202 207 cursor = self._get_cursor()
203 208 if rep['parent_header']['msg_id'] == self._call_tip_id and \
204 209 cursor.position() == self._call_tip_pos:
205 210 doc = rep['content']['docstring']
206 211 if doc:
207 212 self._call_tip_widget.show_docstring(doc)
208 213
209 214 def _handle_pyout(self, msg):
210 215 """ Handle display hook output.
211 216 """
212 217 self._append_plain_text(msg['content']['data'] + '\n')
213 218
214 219 def _handle_stream(self, msg):
215 220 """ Handle stdout, stderr, and stdin.
216 221 """
217 222 self._append_plain_text(msg['content']['data'])
218 223 self._control.moveCursor(QtGui.QTextCursor.End)
219 224
220 225 def _started_channels(self):
221 226 """ Called when the KernelManager channels have started listening or
222 227 when the frontend is assigned an already listening KernelManager.
223 228 """
224 229 self._reset()
225 230 self._append_plain_text(self._get_banner())
226 231 self._show_interpreter_prompt()
227 232
228 233 def _stopped_channels(self):
229 234 """ Called when the KernelManager channels have stopped listening or
230 235 when a listening KernelManager is removed from the frontend.
231 236 """
232 237 # FIXME: Print a message here?
233 238 pass
234 239
235 240 #---------------------------------------------------------------------------
236 241 # 'FrontendWidget' interface
237 242 #---------------------------------------------------------------------------
238 243
239 244 def execute_file(self, path, hidden=False):
240 245 """ Attempts to execute file with 'path'. If 'hidden', no output is
241 246 shown.
242 247 """
243 248 self.execute('execfile("%s")' % path, hidden=hidden)
244 249
245 250 #---------------------------------------------------------------------------
246 251 # 'FrontendWidget' protected interface
247 252 #---------------------------------------------------------------------------
248 253
249 254 def _call_tip(self):
250 255 """ Shows a call tip, if appropriate, at the current cursor location.
251 256 """
252 257 # Decide if it makes sense to show a call tip
253 258 cursor = self._get_cursor()
254 259 cursor.movePosition(QtGui.QTextCursor.Left)
255 260 document = self._control.document()
256 261 if document.characterAt(cursor.position()).toAscii() != '(':
257 262 return False
258 263 context = self._get_context(cursor)
259 264 if not context:
260 265 return False
261 266
262 267 # Send the metadata request to the kernel
263 268 name = '.'.join(context)
264 269 self._call_tip_id = self.kernel_manager.xreq_channel.object_info(name)
265 270 self._call_tip_pos = self._get_cursor().position()
266 271 return True
267 272
268 273 def _complete(self):
269 274 """ Performs completion at the current cursor location.
270 275 """
271 276 # Decide if it makes sense to do completion
272 277 context = self._get_context()
273 278 if not context:
274 279 return False
275 280
276 281 # Send the completion request to the kernel
277 282 text = '.'.join(context)
278 283 self._complete_id = self.kernel_manager.xreq_channel.complete(
279 284 text, self._get_input_buffer_cursor_line(), self.input_buffer)
280 285 self._complete_pos = self._get_cursor().position()
281 286 return True
282 287
283 288 def _get_banner(self):
284 289 """ Gets a banner to display at the beginning of a session.
285 290 """
286 291 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
287 292 '"license" for more information.'
288 293 return banner % (sys.version, sys.platform)
289 294
290 295 def _get_context(self, cursor=None):
291 296 """ Gets the context at the current cursor location.
292 297 """
293 298 if cursor is None:
294 299 cursor = self._get_cursor()
295 300 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
296 301 QtGui.QTextCursor.KeepAnchor)
297 302 text = str(cursor.selection().toPlainText())
298 303 return self._completion_lexer.get_context(text)
299 304
300 305 def _interrupt_kernel(self):
301 306 """ Attempts to the interrupt the kernel.
302 307 """
303 308 if self.kernel_manager.has_kernel:
304 309 self.kernel_manager.signal_kernel(signal.SIGINT)
305 310 else:
306 311 self._append_plain_text('Kernel process is either remote or '
307 312 'unspecified. Cannot interrupt.\n')
308 313
309 314 def _process_execute_abort(self, msg):
310 315 """ Process a reply for an aborted execution request.
311 316 """
312 317 self._append_plain_text("ERROR: execution aborted\n")
313 318
314 319 def _process_execute_error(self, msg):
315 320 """ Process a reply for an execution request that resulted in an error.
316 321 """
317 322 content = msg['content']
318 323 traceback = ''.join(content['traceback'])
319 324 self._append_plain_text(traceback)
320 325
321 326 def _process_execute_ok(self, msg):
322 327 """ Process a reply for a successful execution equest.
323 328 """
324 329 # The basic FrontendWidget doesn't handle payloads, as they are a
325 330 # mechanism for going beyond the standard Python interpreter model.
326 331 pass
327 332
328 333 def _show_interpreter_prompt(self):
329 334 """ Shows a prompt for the interpreter.
330 335 """
331 336 self._show_prompt('>>> ')
332 337
333 338 #------ Signal handlers ----------------------------------------------------
334 339
335 340 def _document_contents_change(self, position, removed, added):
336 341 """ Called whenever the document's content changes. Display a call tip
337 342 if appropriate.
338 343 """
339 344 # Calculate where the cursor should be *after* the change:
340 345 position += added
341 346
342 347 document = self._control.document()
343 348 if position == self._get_cursor().position():
344 349 self._call_tip()
General Comments 0
You need to be logged in to leave comments. Login now