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