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