##// END OF EJS Templates
BUG: Ctrl+K and Ctrl+U shortcuts unsafe in Qt console....
epatters -
Show More
@@ -1,1838 +1,1842 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 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 371
372 372 # FIXME: this line, on qt 4.8.5, crashes b/c NativeGesture is not
373 373 # available. Disabling for now, until we sort out a cleaner solution.
374 374 # See https://github.com/ipython/ipython/pull/1150 for details.
375 375 #elif etype in [QtCore.QEvent.Wheel, QtCore.QEvent.NativeGesture] and \
376 376
377 377 elif etype == QtCore.QEvent.Wheel 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 cursor.clearSelection()
1065 1066 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
1066 1067 QtGui.QTextCursor.KeepAnchor)
1067 1068 if not cursor.hasSelection():
1068 1069 # Line deletion (remove continuation prompt)
1069 1070 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1070 1071 QtGui.QTextCursor.KeepAnchor)
1071 1072 cursor.movePosition(QtGui.QTextCursor.Right,
1072 1073 QtGui.QTextCursor.KeepAnchor,
1073 1074 len(self._continuation_prompt))
1074 1075 self._kill_ring.kill_cursor(cursor)
1076 self._set_cursor(cursor)
1075 1077 intercepted = True
1076 1078
1077 1079 elif key == QtCore.Qt.Key_L:
1078 1080 self.prompt_to_top()
1079 1081 intercepted = True
1080 1082
1081 1083 elif key == QtCore.Qt.Key_O:
1082 1084 if self._page_control and self._page_control.isVisible():
1083 1085 self._page_control.setFocus()
1084 1086 intercepted = True
1085 1087
1086 1088 elif key == QtCore.Qt.Key_U:
1087 1089 if self._in_buffer(position):
1090 cursor.clearSelection()
1088 1091 start_line = cursor.blockNumber()
1089 1092 if start_line == self._get_prompt_cursor().blockNumber():
1090 1093 offset = len(self._prompt)
1091 1094 else:
1092 1095 offset = len(self._continuation_prompt)
1093 1096 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1094 1097 QtGui.QTextCursor.KeepAnchor)
1095 1098 cursor.movePosition(QtGui.QTextCursor.Right,
1096 1099 QtGui.QTextCursor.KeepAnchor, offset)
1097 1100 self._kill_ring.kill_cursor(cursor)
1101 self._set_cursor(cursor)
1098 1102 intercepted = True
1099 1103
1100 1104 elif key == QtCore.Qt.Key_Y:
1101 1105 self._keep_cursor_in_buffer()
1102 1106 self._kill_ring.yank()
1103 1107 intercepted = True
1104 1108
1105 1109 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
1106 1110 if key == QtCore.Qt.Key_Backspace:
1107 1111 cursor = self._get_word_start_cursor(position)
1108 1112 else: # key == QtCore.Qt.Key_Delete
1109 1113 cursor = self._get_word_end_cursor(position)
1110 1114 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1111 1115 self._kill_ring.kill_cursor(cursor)
1112 1116 intercepted = True
1113 1117
1114 1118 #------ Alt modifier ---------------------------------------------------
1115 1119
1116 1120 elif alt_down:
1117 1121 if key == QtCore.Qt.Key_B:
1118 1122 self._set_cursor(self._get_word_start_cursor(position))
1119 1123 intercepted = True
1120 1124
1121 1125 elif key == QtCore.Qt.Key_F:
1122 1126 self._set_cursor(self._get_word_end_cursor(position))
1123 1127 intercepted = True
1124 1128
1125 1129 elif key == QtCore.Qt.Key_Y:
1126 1130 self._kill_ring.rotate()
1127 1131 intercepted = True
1128 1132
1129 1133 elif key == QtCore.Qt.Key_Backspace:
1130 1134 cursor = self._get_word_start_cursor(position)
1131 1135 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1132 1136 self._kill_ring.kill_cursor(cursor)
1133 1137 intercepted = True
1134 1138
1135 1139 elif key == QtCore.Qt.Key_D:
1136 1140 cursor = self._get_word_end_cursor(position)
1137 1141 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1138 1142 self._kill_ring.kill_cursor(cursor)
1139 1143 intercepted = True
1140 1144
1141 1145 elif key == QtCore.Qt.Key_Delete:
1142 1146 intercepted = True
1143 1147
1144 1148 elif key == QtCore.Qt.Key_Greater:
1145 1149 self._control.moveCursor(QtGui.QTextCursor.End)
1146 1150 intercepted = True
1147 1151
1148 1152 elif key == QtCore.Qt.Key_Less:
1149 1153 self._control.setTextCursor(self._get_prompt_cursor())
1150 1154 intercepted = True
1151 1155
1152 1156 #------ No modifiers ---------------------------------------------------
1153 1157
1154 1158 else:
1155 1159 if shift_down:
1156 1160 anchormode = QtGui.QTextCursor.KeepAnchor
1157 1161 else:
1158 1162 anchormode = QtGui.QTextCursor.MoveAnchor
1159 1163
1160 1164 if key == QtCore.Qt.Key_Escape:
1161 1165 self._keyboard_quit()
1162 1166 intercepted = True
1163 1167
1164 1168 elif key == QtCore.Qt.Key_Up:
1165 1169 if self._reading or not self._up_pressed(shift_down):
1166 1170 intercepted = True
1167 1171 else:
1168 1172 prompt_line = self._get_prompt_cursor().blockNumber()
1169 1173 intercepted = cursor.blockNumber() <= prompt_line
1170 1174
1171 1175 elif key == QtCore.Qt.Key_Down:
1172 1176 if self._reading or not self._down_pressed(shift_down):
1173 1177 intercepted = True
1174 1178 else:
1175 1179 end_line = self._get_end_cursor().blockNumber()
1176 1180 intercepted = cursor.blockNumber() == end_line
1177 1181
1178 1182 elif key == QtCore.Qt.Key_Tab:
1179 1183 if not self._reading:
1180 1184 if self._tab_pressed():
1181 1185 # real tab-key, insert four spaces
1182 1186 cursor.insertText(' '*4)
1183 1187 intercepted = True
1184 1188
1185 1189 elif key == QtCore.Qt.Key_Left:
1186 1190
1187 1191 # Move to the previous line
1188 1192 line, col = cursor.blockNumber(), cursor.columnNumber()
1189 1193 if line > self._get_prompt_cursor().blockNumber() and \
1190 1194 col == len(self._continuation_prompt):
1191 1195 self._control.moveCursor(QtGui.QTextCursor.PreviousBlock,
1192 1196 mode=anchormode)
1193 1197 self._control.moveCursor(QtGui.QTextCursor.EndOfBlock,
1194 1198 mode=anchormode)
1195 1199 intercepted = True
1196 1200
1197 1201 # Regular left movement
1198 1202 else:
1199 1203 intercepted = not self._in_buffer(position - 1)
1200 1204
1201 1205 elif key == QtCore.Qt.Key_Right:
1202 1206 original_block_number = cursor.blockNumber()
1203 1207 cursor.movePosition(QtGui.QTextCursor.Right,
1204 1208 mode=anchormode)
1205 1209 if cursor.blockNumber() != original_block_number:
1206 1210 cursor.movePosition(QtGui.QTextCursor.Right,
1207 1211 n=len(self._continuation_prompt),
1208 1212 mode=anchormode)
1209 1213 self._set_cursor(cursor)
1210 1214 intercepted = True
1211 1215
1212 1216 elif key == QtCore.Qt.Key_Home:
1213 1217 start_line = cursor.blockNumber()
1214 1218 if start_line == self._get_prompt_cursor().blockNumber():
1215 1219 start_pos = self._prompt_pos
1216 1220 else:
1217 1221 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1218 1222 QtGui.QTextCursor.KeepAnchor)
1219 1223 start_pos = cursor.position()
1220 1224 start_pos += len(self._continuation_prompt)
1221 1225 cursor.setPosition(position)
1222 1226 if shift_down and self._in_buffer(position):
1223 1227 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
1224 1228 else:
1225 1229 cursor.setPosition(start_pos)
1226 1230 self._set_cursor(cursor)
1227 1231 intercepted = True
1228 1232
1229 1233 elif key == QtCore.Qt.Key_Backspace:
1230 1234
1231 1235 # Line deletion (remove continuation prompt)
1232 1236 line, col = cursor.blockNumber(), cursor.columnNumber()
1233 1237 if not self._reading and \
1234 1238 col == len(self._continuation_prompt) and \
1235 1239 line > self._get_prompt_cursor().blockNumber():
1236 1240 cursor.beginEditBlock()
1237 1241 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1238 1242 QtGui.QTextCursor.KeepAnchor)
1239 1243 cursor.removeSelectedText()
1240 1244 cursor.deletePreviousChar()
1241 1245 cursor.endEditBlock()
1242 1246 intercepted = True
1243 1247
1244 1248 # Regular backwards deletion
1245 1249 else:
1246 1250 anchor = cursor.anchor()
1247 1251 if anchor == position:
1248 1252 intercepted = not self._in_buffer(position - 1)
1249 1253 else:
1250 1254 intercepted = not self._in_buffer(min(anchor, position))
1251 1255
1252 1256 elif key == QtCore.Qt.Key_Delete:
1253 1257
1254 1258 # Line deletion (remove continuation prompt)
1255 1259 if not self._reading and self._in_buffer(position) and \
1256 1260 cursor.atBlockEnd() and not cursor.hasSelection():
1257 1261 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1258 1262 QtGui.QTextCursor.KeepAnchor)
1259 1263 cursor.movePosition(QtGui.QTextCursor.Right,
1260 1264 QtGui.QTextCursor.KeepAnchor,
1261 1265 len(self._continuation_prompt))
1262 1266 cursor.removeSelectedText()
1263 1267 intercepted = True
1264 1268
1265 1269 # Regular forwards deletion:
1266 1270 else:
1267 1271 anchor = cursor.anchor()
1268 1272 intercepted = (not self._in_buffer(anchor) or
1269 1273 not self._in_buffer(position))
1270 1274
1271 1275 # Don't move the cursor if Control/Cmd is pressed to allow copy-paste
1272 1276 # using the keyboard in any part of the buffer. Also, permit scrolling
1273 1277 # with Page Up/Down keys. Finally, if we're executing, don't move the
1274 1278 # cursor (if even this made sense, we can't guarantee that the prompt
1275 1279 # position is still valid due to text truncation).
1276 1280 if not (self._control_key_down(event.modifiers(), include_command=True)
1277 1281 or key in (QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown)
1278 1282 or (self._executing and not self._reading)):
1279 1283 self._keep_cursor_in_buffer()
1280 1284
1281 1285 return intercepted
1282 1286
1283 1287 def _event_filter_page_keypress(self, event):
1284 1288 """ Filter key events for the paging widget to create console-like
1285 1289 interface.
1286 1290 """
1287 1291 key = event.key()
1288 1292 ctrl_down = self._control_key_down(event.modifiers())
1289 1293 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1290 1294
1291 1295 if ctrl_down:
1292 1296 if key == QtCore.Qt.Key_O:
1293 1297 self._control.setFocus()
1294 1298 intercept = True
1295 1299
1296 1300 elif alt_down:
1297 1301 if key == QtCore.Qt.Key_Greater:
1298 1302 self._page_control.moveCursor(QtGui.QTextCursor.End)
1299 1303 intercepted = True
1300 1304
1301 1305 elif key == QtCore.Qt.Key_Less:
1302 1306 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1303 1307 intercepted = True
1304 1308
1305 1309 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
1306 1310 if self._splitter:
1307 1311 self._page_control.hide()
1308 1312 self._control.setFocus()
1309 1313 else:
1310 1314 self.layout().setCurrentWidget(self._control)
1311 1315 return True
1312 1316
1313 1317 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return,
1314 1318 QtCore.Qt.Key_Tab):
1315 1319 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1316 1320 QtCore.Qt.Key_PageDown,
1317 1321 QtCore.Qt.NoModifier)
1318 1322 QtGui.qApp.sendEvent(self._page_control, new_event)
1319 1323 return True
1320 1324
1321 1325 elif key == QtCore.Qt.Key_Backspace:
1322 1326 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1323 1327 QtCore.Qt.Key_PageUp,
1324 1328 QtCore.Qt.NoModifier)
1325 1329 QtGui.qApp.sendEvent(self._page_control, new_event)
1326 1330 return True
1327 1331
1328 1332 return False
1329 1333
1330 1334 def _format_as_columns(self, items, separator=' '):
1331 1335 """ Transform a list of strings into a single string with columns.
1332 1336
1333 1337 Parameters
1334 1338 ----------
1335 1339 items : sequence of strings
1336 1340 The strings to process.
1337 1341
1338 1342 separator : str, optional [default is two spaces]
1339 1343 The string that separates columns.
1340 1344
1341 1345 Returns
1342 1346 -------
1343 1347 The formatted string.
1344 1348 """
1345 1349 # Calculate the number of characters available.
1346 1350 width = self._control.viewport().width()
1347 1351 char_width = QtGui.QFontMetrics(self.font).width(' ')
1348 1352 displaywidth = max(10, (width / char_width) - 1)
1349 1353
1350 1354 return columnize(items, separator, displaywidth)
1351 1355
1352 1356 def _get_block_plain_text(self, block):
1353 1357 """ Given a QTextBlock, return its unformatted text.
1354 1358 """
1355 1359 cursor = QtGui.QTextCursor(block)
1356 1360 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1357 1361 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1358 1362 QtGui.QTextCursor.KeepAnchor)
1359 1363 return cursor.selection().toPlainText()
1360 1364
1361 1365 def _get_cursor(self):
1362 1366 """ Convenience method that returns a cursor for the current position.
1363 1367 """
1364 1368 return self._control.textCursor()
1365 1369
1366 1370 def _get_end_cursor(self):
1367 1371 """ Convenience method that returns a cursor for the last character.
1368 1372 """
1369 1373 cursor = self._control.textCursor()
1370 1374 cursor.movePosition(QtGui.QTextCursor.End)
1371 1375 return cursor
1372 1376
1373 1377 def _get_input_buffer_cursor_column(self):
1374 1378 """ Returns the column of the cursor in the input buffer, excluding the
1375 1379 contribution by the prompt, or -1 if there is no such column.
1376 1380 """
1377 1381 prompt = self._get_input_buffer_cursor_prompt()
1378 1382 if prompt is None:
1379 1383 return -1
1380 1384 else:
1381 1385 cursor = self._control.textCursor()
1382 1386 return cursor.columnNumber() - len(prompt)
1383 1387
1384 1388 def _get_input_buffer_cursor_line(self):
1385 1389 """ Returns the text of the line of the input buffer that contains the
1386 1390 cursor, or None if there is no such line.
1387 1391 """
1388 1392 prompt = self._get_input_buffer_cursor_prompt()
1389 1393 if prompt is None:
1390 1394 return None
1391 1395 else:
1392 1396 cursor = self._control.textCursor()
1393 1397 text = self._get_block_plain_text(cursor.block())
1394 1398 return text[len(prompt):]
1395 1399
1396 1400 def _get_input_buffer_cursor_prompt(self):
1397 1401 """ Returns the (plain text) prompt for line of the input buffer that
1398 1402 contains the cursor, or None if there is no such line.
1399 1403 """
1400 1404 if self._executing:
1401 1405 return None
1402 1406 cursor = self._control.textCursor()
1403 1407 if cursor.position() >= self._prompt_pos:
1404 1408 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1405 1409 return self._prompt
1406 1410 else:
1407 1411 return self._continuation_prompt
1408 1412 else:
1409 1413 return None
1410 1414
1411 1415 def _get_prompt_cursor(self):
1412 1416 """ Convenience method that returns a cursor for the prompt position.
1413 1417 """
1414 1418 cursor = self._control.textCursor()
1415 1419 cursor.setPosition(self._prompt_pos)
1416 1420 return cursor
1417 1421
1418 1422 def _get_selection_cursor(self, start, end):
1419 1423 """ Convenience method that returns a cursor with text selected between
1420 1424 the positions 'start' and 'end'.
1421 1425 """
1422 1426 cursor = self._control.textCursor()
1423 1427 cursor.setPosition(start)
1424 1428 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1425 1429 return cursor
1426 1430
1427 1431 def _get_word_start_cursor(self, position):
1428 1432 """ Find the start of the word to the left the given position. If a
1429 1433 sequence of non-word characters precedes the first word, skip over
1430 1434 them. (This emulates the behavior of bash, emacs, etc.)
1431 1435 """
1432 1436 document = self._control.document()
1433 1437 position -= 1
1434 1438 while position >= self._prompt_pos and \
1435 1439 not is_letter_or_number(document.characterAt(position)):
1436 1440 position -= 1
1437 1441 while position >= self._prompt_pos and \
1438 1442 is_letter_or_number(document.characterAt(position)):
1439 1443 position -= 1
1440 1444 cursor = self._control.textCursor()
1441 1445 cursor.setPosition(position + 1)
1442 1446 return cursor
1443 1447
1444 1448 def _get_word_end_cursor(self, position):
1445 1449 """ Find the end of the word to the right the given position. If a
1446 1450 sequence of non-word characters precedes the first word, skip over
1447 1451 them. (This emulates the behavior of bash, emacs, etc.)
1448 1452 """
1449 1453 document = self._control.document()
1450 1454 end = self._get_end_cursor().position()
1451 1455 while position < end and \
1452 1456 not is_letter_or_number(document.characterAt(position)):
1453 1457 position += 1
1454 1458 while position < end and \
1455 1459 is_letter_or_number(document.characterAt(position)):
1456 1460 position += 1
1457 1461 cursor = self._control.textCursor()
1458 1462 cursor.setPosition(position)
1459 1463 return cursor
1460 1464
1461 1465 def _insert_continuation_prompt(self, cursor):
1462 1466 """ Inserts new continuation prompt using the specified cursor.
1463 1467 """
1464 1468 if self._continuation_prompt_html is None:
1465 1469 self._insert_plain_text(cursor, self._continuation_prompt)
1466 1470 else:
1467 1471 self._continuation_prompt = self._insert_html_fetching_plain_text(
1468 1472 cursor, self._continuation_prompt_html)
1469 1473
1470 1474 def _insert_html(self, cursor, html):
1471 1475 """ Inserts HTML using the specified cursor in such a way that future
1472 1476 formatting is unaffected.
1473 1477 """
1474 1478 cursor.beginEditBlock()
1475 1479 cursor.insertHtml(html)
1476 1480
1477 1481 # After inserting HTML, the text document "remembers" it's in "html
1478 1482 # mode", which means that subsequent calls adding plain text will result
1479 1483 # in unwanted formatting, lost tab characters, etc. The following code
1480 1484 # hacks around this behavior, which I consider to be a bug in Qt, by
1481 1485 # (crudely) resetting the document's style state.
1482 1486 cursor.movePosition(QtGui.QTextCursor.Left,
1483 1487 QtGui.QTextCursor.KeepAnchor)
1484 1488 if cursor.selection().toPlainText() == ' ':
1485 1489 cursor.removeSelectedText()
1486 1490 else:
1487 1491 cursor.movePosition(QtGui.QTextCursor.Right)
1488 1492 cursor.insertText(' ', QtGui.QTextCharFormat())
1489 1493 cursor.endEditBlock()
1490 1494
1491 1495 def _insert_html_fetching_plain_text(self, cursor, html):
1492 1496 """ Inserts HTML using the specified cursor, then returns its plain text
1493 1497 version.
1494 1498 """
1495 1499 cursor.beginEditBlock()
1496 1500 cursor.removeSelectedText()
1497 1501
1498 1502 start = cursor.position()
1499 1503 self._insert_html(cursor, html)
1500 1504 end = cursor.position()
1501 1505 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1502 1506 text = cursor.selection().toPlainText()
1503 1507
1504 1508 cursor.setPosition(end)
1505 1509 cursor.endEditBlock()
1506 1510 return text
1507 1511
1508 1512 def _insert_plain_text(self, cursor, text):
1509 1513 """ Inserts plain text using the specified cursor, processing ANSI codes
1510 1514 if enabled.
1511 1515 """
1512 1516 cursor.beginEditBlock()
1513 1517 if self.ansi_codes:
1514 1518 for substring in self._ansi_processor.split_string(text):
1515 1519 for act in self._ansi_processor.actions:
1516 1520
1517 1521 # Unlike real terminal emulators, we don't distinguish
1518 1522 # between the screen and the scrollback buffer. A screen
1519 1523 # erase request clears everything.
1520 1524 if act.action == 'erase' and act.area == 'screen':
1521 1525 cursor.select(QtGui.QTextCursor.Document)
1522 1526 cursor.removeSelectedText()
1523 1527
1524 1528 # Simulate a form feed by scrolling just past the last line.
1525 1529 elif act.action == 'scroll' and act.unit == 'page':
1526 1530 cursor.insertText('\n')
1527 1531 cursor.endEditBlock()
1528 1532 self._set_top_cursor(cursor)
1529 1533 cursor.joinPreviousEditBlock()
1530 1534 cursor.deletePreviousChar()
1531 1535
1532 1536 elif act.action == 'carriage-return':
1533 1537 cursor.movePosition(
1534 1538 cursor.StartOfLine, cursor.KeepAnchor)
1535 1539
1536 1540 elif act.action == 'beep':
1537 1541 QtGui.qApp.beep()
1538 1542
1539 1543 format = self._ansi_processor.get_format()
1540 1544 cursor.insertText(substring, format)
1541 1545 else:
1542 1546 cursor.insertText(text)
1543 1547 cursor.endEditBlock()
1544 1548
1545 1549 def _insert_plain_text_into_buffer(self, cursor, text):
1546 1550 """ Inserts text into the input buffer using the specified cursor (which
1547 1551 must be in the input buffer), ensuring that continuation prompts are
1548 1552 inserted as necessary.
1549 1553 """
1550 1554 lines = text.splitlines(True)
1551 1555 if lines:
1552 1556 cursor.beginEditBlock()
1553 1557 cursor.insertText(lines[0])
1554 1558 for line in lines[1:]:
1555 1559 if self._continuation_prompt_html is None:
1556 1560 cursor.insertText(self._continuation_prompt)
1557 1561 else:
1558 1562 self._continuation_prompt = \
1559 1563 self._insert_html_fetching_plain_text(
1560 1564 cursor, self._continuation_prompt_html)
1561 1565 cursor.insertText(line)
1562 1566 cursor.endEditBlock()
1563 1567
1564 1568 def _in_buffer(self, position=None):
1565 1569 """ Returns whether the current cursor (or, if specified, a position) is
1566 1570 inside the editing region.
1567 1571 """
1568 1572 cursor = self._control.textCursor()
1569 1573 if position is None:
1570 1574 position = cursor.position()
1571 1575 else:
1572 1576 cursor.setPosition(position)
1573 1577 line = cursor.blockNumber()
1574 1578 prompt_line = self._get_prompt_cursor().blockNumber()
1575 1579 if line == prompt_line:
1576 1580 return position >= self._prompt_pos
1577 1581 elif line > prompt_line:
1578 1582 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1579 1583 prompt_pos = cursor.position() + len(self._continuation_prompt)
1580 1584 return position >= prompt_pos
1581 1585 return False
1582 1586
1583 1587 def _keep_cursor_in_buffer(self):
1584 1588 """ Ensures that the cursor is inside the editing region. Returns
1585 1589 whether the cursor was moved.
1586 1590 """
1587 1591 moved = not self._in_buffer()
1588 1592 if moved:
1589 1593 cursor = self._control.textCursor()
1590 1594 cursor.movePosition(QtGui.QTextCursor.End)
1591 1595 self._control.setTextCursor(cursor)
1592 1596 return moved
1593 1597
1594 1598 def _keyboard_quit(self):
1595 1599 """ Cancels the current editing task ala Ctrl-G in Emacs.
1596 1600 """
1597 1601 if self._text_completing_pos:
1598 1602 self._cancel_text_completion()
1599 1603 else:
1600 1604 self.input_buffer = ''
1601 1605
1602 1606 def _page(self, text, html=False):
1603 1607 """ Displays text using the pager if it exceeds the height of the
1604 1608 viewport.
1605 1609
1606 1610 Parameters:
1607 1611 -----------
1608 1612 html : bool, optional (default False)
1609 1613 If set, the text will be interpreted as HTML instead of plain text.
1610 1614 """
1611 1615 line_height = QtGui.QFontMetrics(self.font).height()
1612 1616 minlines = self._control.viewport().height() / line_height
1613 1617 if self.paging != 'none' and \
1614 1618 re.match("(?:[^\n]*\n){%i}" % minlines, text):
1615 1619 if self.paging == 'custom':
1616 1620 self.custom_page_requested.emit(text)
1617 1621 else:
1618 1622 self._page_control.clear()
1619 1623 cursor = self._page_control.textCursor()
1620 1624 if html:
1621 1625 self._insert_html(cursor, text)
1622 1626 else:
1623 1627 self._insert_plain_text(cursor, text)
1624 1628 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1625 1629
1626 1630 self._page_control.viewport().resize(self._control.size())
1627 1631 if self._splitter:
1628 1632 self._page_control.show()
1629 1633 self._page_control.setFocus()
1630 1634 else:
1631 1635 self.layout().setCurrentWidget(self._page_control)
1632 1636 elif html:
1633 1637 self._append_plain_html(text)
1634 1638 else:
1635 1639 self._append_plain_text(text)
1636 1640
1637 1641 def _prompt_finished(self):
1638 1642 """ Called immediately after a prompt is finished, i.e. when some input
1639 1643 will be processed and a new prompt displayed.
1640 1644 """
1641 1645 self._control.setReadOnly(True)
1642 1646 self._prompt_finished_hook()
1643 1647
1644 1648 def _prompt_started(self):
1645 1649 """ Called immediately after a new prompt is displayed.
1646 1650 """
1647 1651 # Temporarily disable the maximum block count to permit undo/redo and
1648 1652 # to ensure that the prompt position does not change due to truncation.
1649 1653 self._control.document().setMaximumBlockCount(0)
1650 1654 self._control.setUndoRedoEnabled(True)
1651 1655
1652 1656 # Work around bug in QPlainTextEdit: input method is not re-enabled
1653 1657 # when read-only is disabled.
1654 1658 self._control.setReadOnly(False)
1655 1659 self._control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1656 1660
1657 1661 if not self._reading:
1658 1662 self._executing = False
1659 1663 self._prompt_started_hook()
1660 1664
1661 1665 # If the input buffer has changed while executing, load it.
1662 1666 if self._input_buffer_pending:
1663 1667 self.input_buffer = self._input_buffer_pending
1664 1668 self._input_buffer_pending = ''
1665 1669
1666 1670 self._control.moveCursor(QtGui.QTextCursor.End)
1667 1671
1668 1672 def _readline(self, prompt='', callback=None):
1669 1673 """ Reads one line of input from the user.
1670 1674
1671 1675 Parameters
1672 1676 ----------
1673 1677 prompt : str, optional
1674 1678 The prompt to print before reading the line.
1675 1679
1676 1680 callback : callable, optional
1677 1681 A callback to execute with the read line. If not specified, input is
1678 1682 read *synchronously* and this method does not return until it has
1679 1683 been read.
1680 1684
1681 1685 Returns
1682 1686 -------
1683 1687 If a callback is specified, returns nothing. Otherwise, returns the
1684 1688 input string with the trailing newline stripped.
1685 1689 """
1686 1690 if self._reading:
1687 1691 raise RuntimeError('Cannot read a line. Widget is already reading.')
1688 1692
1689 1693 if not callback and not self.isVisible():
1690 1694 # If the user cannot see the widget, this function cannot return.
1691 1695 raise RuntimeError('Cannot synchronously read a line if the widget '
1692 1696 'is not visible!')
1693 1697
1694 1698 self._reading = True
1695 1699 self._show_prompt(prompt, newline=False)
1696 1700
1697 1701 if callback is None:
1698 1702 self._reading_callback = None
1699 1703 while self._reading:
1700 1704 QtCore.QCoreApplication.processEvents()
1701 1705 return self._get_input_buffer(force=True).rstrip('\n')
1702 1706
1703 1707 else:
1704 1708 self._reading_callback = lambda: \
1705 1709 callback(self._get_input_buffer(force=True).rstrip('\n'))
1706 1710
1707 1711 def _set_continuation_prompt(self, prompt, html=False):
1708 1712 """ Sets the continuation prompt.
1709 1713
1710 1714 Parameters
1711 1715 ----------
1712 1716 prompt : str
1713 1717 The prompt to show when more input is needed.
1714 1718
1715 1719 html : bool, optional (default False)
1716 1720 If set, the prompt will be inserted as formatted HTML. Otherwise,
1717 1721 the prompt will be treated as plain text, though ANSI color codes
1718 1722 will be handled.
1719 1723 """
1720 1724 if html:
1721 1725 self._continuation_prompt_html = prompt
1722 1726 else:
1723 1727 self._continuation_prompt = prompt
1724 1728 self._continuation_prompt_html = None
1725 1729
1726 1730 def _set_cursor(self, cursor):
1727 1731 """ Convenience method to set the current cursor.
1728 1732 """
1729 1733 self._control.setTextCursor(cursor)
1730 1734
1731 1735 def _set_top_cursor(self, cursor):
1732 1736 """ Scrolls the viewport so that the specified cursor is at the top.
1733 1737 """
1734 1738 scrollbar = self._control.verticalScrollBar()
1735 1739 scrollbar.setValue(scrollbar.maximum())
1736 1740 original_cursor = self._control.textCursor()
1737 1741 self._control.setTextCursor(cursor)
1738 1742 self._control.ensureCursorVisible()
1739 1743 self._control.setTextCursor(original_cursor)
1740 1744
1741 1745 def _show_prompt(self, prompt=None, html=False, newline=True):
1742 1746 """ Writes a new prompt at the end of the buffer.
1743 1747
1744 1748 Parameters
1745 1749 ----------
1746 1750 prompt : str, optional
1747 1751 The prompt to show. If not specified, the previous prompt is used.
1748 1752
1749 1753 html : bool, optional (default False)
1750 1754 Only relevant when a prompt is specified. If set, the prompt will
1751 1755 be inserted as formatted HTML. Otherwise, the prompt will be treated
1752 1756 as plain text, though ANSI color codes will be handled.
1753 1757
1754 1758 newline : bool, optional (default True)
1755 1759 If set, a new line will be written before showing the prompt if
1756 1760 there is not already a newline at the end of the buffer.
1757 1761 """
1758 1762 # Save the current end position to support _append*(before_prompt=True).
1759 1763 cursor = self._get_end_cursor()
1760 1764 self._append_before_prompt_pos = cursor.position()
1761 1765
1762 1766 # Insert a preliminary newline, if necessary.
1763 1767 if newline and cursor.position() > 0:
1764 1768 cursor.movePosition(QtGui.QTextCursor.Left,
1765 1769 QtGui.QTextCursor.KeepAnchor)
1766 1770 if cursor.selection().toPlainText() != '\n':
1767 1771 self._append_plain_text('\n')
1768 1772
1769 1773 # Write the prompt.
1770 1774 self._append_plain_text(self._prompt_sep)
1771 1775 if prompt is None:
1772 1776 if self._prompt_html is None:
1773 1777 self._append_plain_text(self._prompt)
1774 1778 else:
1775 1779 self._append_html(self._prompt_html)
1776 1780 else:
1777 1781 if html:
1778 1782 self._prompt = self._append_html_fetching_plain_text(prompt)
1779 1783 self._prompt_html = prompt
1780 1784 else:
1781 1785 self._append_plain_text(prompt)
1782 1786 self._prompt = prompt
1783 1787 self._prompt_html = None
1784 1788
1785 1789 self._prompt_pos = self._get_end_cursor().position()
1786 1790 self._prompt_started()
1787 1791
1788 1792 #------ Signal handlers ----------------------------------------------------
1789 1793
1790 1794 def _adjust_scrollbars(self):
1791 1795 """ Expands the vertical scrollbar beyond the range set by Qt.
1792 1796 """
1793 1797 # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
1794 1798 # and qtextedit.cpp.
1795 1799 document = self._control.document()
1796 1800 scrollbar = self._control.verticalScrollBar()
1797 1801 viewport_height = self._control.viewport().height()
1798 1802 if isinstance(self._control, QtGui.QPlainTextEdit):
1799 1803 maximum = max(0, document.lineCount() - 1)
1800 1804 step = viewport_height / self._control.fontMetrics().lineSpacing()
1801 1805 else:
1802 1806 # QTextEdit does not do line-based layout and blocks will not in
1803 1807 # general have the same height. Therefore it does not make sense to
1804 1808 # attempt to scroll in line height increments.
1805 1809 maximum = document.size().height()
1806 1810 step = viewport_height
1807 1811 diff = maximum - scrollbar.maximum()
1808 1812 scrollbar.setRange(0, maximum)
1809 1813 scrollbar.setPageStep(step)
1810 1814
1811 1815 # Compensate for undesirable scrolling that occurs automatically due to
1812 1816 # maximumBlockCount() text truncation.
1813 1817 if diff < 0 and document.blockCount() == document.maximumBlockCount():
1814 1818 scrollbar.setValue(scrollbar.value() + diff)
1815 1819
1816 1820 def _cursor_position_changed(self):
1817 1821 """ Clears the temporary buffer based on the cursor position.
1818 1822 """
1819 1823 if self._text_completing_pos:
1820 1824 document = self._control.document()
1821 1825 if self._text_completing_pos < document.characterCount():
1822 1826 cursor = self._control.textCursor()
1823 1827 pos = cursor.position()
1824 1828 text_cursor = self._control.textCursor()
1825 1829 text_cursor.setPosition(self._text_completing_pos)
1826 1830 if pos < self._text_completing_pos or \
1827 1831 cursor.blockNumber() > text_cursor.blockNumber():
1828 1832 self._clear_temporary_buffer()
1829 1833 self._text_completing_pos = 0
1830 1834 else:
1831 1835 self._clear_temporary_buffer()
1832 1836 self._text_completing_pos = 0
1833 1837
1834 1838 def _custom_context_menu_requested(self, pos):
1835 1839 """ Shows a context menu at the given QPoint (in widget coordinates).
1836 1840 """
1837 1841 menu = self._context_menu_make(pos)
1838 1842 menu.exec_(self._control.mapToGlobal(pos))
General Comments 0
You need to be logged in to leave comments. Login now