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