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