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