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