##// END OF EJS Templates
Fix qtconsole input prompt accounting...
Pankaj Pandey -
Show More
@@ -1,2133 +1,2136 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 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 if before_prompt and not self._executing:
922 if before_prompt and (self._reading or 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 1494 def _on_flush_pending_stream_timer(self):
1495 1495 """ Flush the pending stream output and change the
1496 1496 prompt position appropriately.
1497 1497 """
1498 1498 cursor = self._control.textCursor()
1499 1499 cursor.movePosition(QtGui.QTextCursor.End)
1500 1500 pos = cursor.position()
1501 1501 self._flush_pending_stream()
1502 1502 cursor.movePosition(QtGui.QTextCursor.End)
1503 1503 diff = cursor.position() - pos
1504 1504 if diff > 0:
1505 1505 self._prompt_pos += diff
1506 1506 self._append_before_prompt_pos += diff
1507 1507
1508 1508 def _flush_pending_stream(self):
1509 1509 """ Flush out pending text into the widget. """
1510 1510 text = self._pending_insert_text
1511 1511 self._pending_insert_text = []
1512 1512 buffer_size = self._control.document().maximumBlockCount()
1513 1513 if buffer_size > 0:
1514 1514 text = self._get_last_lines_from_list(text, buffer_size)
1515 1515 text = ''.join(text)
1516 1516 t = time.time()
1517 1517 self._insert_plain_text(self._get_end_cursor(), text, flush=True)
1518 1518 # Set the flush interval to equal the maximum time to update text.
1519 1519 self._pending_text_flush_interval.setInterval(max(100,
1520 1520 (time.time()-t)*1000))
1521 1521
1522 1522 def _format_as_columns(self, items, separator=' '):
1523 1523 """ Transform a list of strings into a single string with columns.
1524 1524
1525 1525 Parameters
1526 1526 ----------
1527 1527 items : sequence of strings
1528 1528 The strings to process.
1529 1529
1530 1530 separator : str, optional [default is two spaces]
1531 1531 The string that separates columns.
1532 1532
1533 1533 Returns
1534 1534 -------
1535 1535 The formatted string.
1536 1536 """
1537 1537 # Calculate the number of characters available.
1538 1538 width = self._control.viewport().width()
1539 1539 char_width = QtGui.QFontMetrics(self.font).width(' ')
1540 1540 displaywidth = max(10, (width / char_width) - 1)
1541 1541
1542 1542 return columnize(items, separator, displaywidth)
1543 1543
1544 1544 def _get_block_plain_text(self, block):
1545 1545 """ Given a QTextBlock, return its unformatted text.
1546 1546 """
1547 1547 cursor = QtGui.QTextCursor(block)
1548 1548 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1549 1549 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1550 1550 QtGui.QTextCursor.KeepAnchor)
1551 1551 return cursor.selection().toPlainText()
1552 1552
1553 1553 def _get_cursor(self):
1554 1554 """ Convenience method that returns a cursor for the current position.
1555 1555 """
1556 1556 return self._control.textCursor()
1557 1557
1558 1558 def _get_end_cursor(self):
1559 1559 """ Convenience method that returns a cursor for the last character.
1560 1560 """
1561 1561 cursor = self._control.textCursor()
1562 1562 cursor.movePosition(QtGui.QTextCursor.End)
1563 1563 return cursor
1564 1564
1565 1565 def _get_input_buffer_cursor_column(self):
1566 1566 """ Returns the column of the cursor in the input buffer, excluding the
1567 1567 contribution by the prompt, or -1 if there is no such column.
1568 1568 """
1569 1569 prompt = self._get_input_buffer_cursor_prompt()
1570 1570 if prompt is None:
1571 1571 return -1
1572 1572 else:
1573 1573 cursor = self._control.textCursor()
1574 1574 return cursor.columnNumber() - len(prompt)
1575 1575
1576 1576 def _get_input_buffer_cursor_line(self):
1577 1577 """ Returns the text of the line of the input buffer that contains the
1578 1578 cursor, or None if there is no such line.
1579 1579 """
1580 1580 prompt = self._get_input_buffer_cursor_prompt()
1581 1581 if prompt is None:
1582 1582 return None
1583 1583 else:
1584 1584 cursor = self._control.textCursor()
1585 1585 text = self._get_block_plain_text(cursor.block())
1586 1586 return text[len(prompt):]
1587 1587
1588 1588 def _get_input_buffer_cursor_pos(self):
1589 1589 """Return the cursor position within the input buffer."""
1590 1590 cursor = self._control.textCursor()
1591 1591 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
1592 1592 input_buffer = cursor.selection().toPlainText()
1593 1593
1594 1594 # Don't count continuation prompts
1595 1595 return len(input_buffer.replace('\n' + self._continuation_prompt, '\n'))
1596 1596
1597 1597 def _get_input_buffer_cursor_prompt(self):
1598 1598 """ Returns the (plain text) prompt for line of the input buffer that
1599 1599 contains the cursor, or None if there is no such line.
1600 1600 """
1601 1601 if self._executing:
1602 1602 return None
1603 1603 cursor = self._control.textCursor()
1604 1604 if cursor.position() >= self._prompt_pos:
1605 1605 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1606 1606 return self._prompt
1607 1607 else:
1608 1608 return self._continuation_prompt
1609 1609 else:
1610 1610 return None
1611 1611
1612 1612 def _get_last_lines(self, text, num_lines, return_count=False):
1613 1613 """ Return last specified number of lines of text (like `tail -n`).
1614 1614 If return_count is True, returns a tuple of clipped text and the
1615 1615 number of lines in the clipped text.
1616 1616 """
1617 1617 pos = len(text)
1618 1618 if pos < num_lines:
1619 1619 if return_count:
1620 1620 return text, text.count('\n') if return_count else text
1621 1621 else:
1622 1622 return text
1623 1623 i = 0
1624 1624 while i < num_lines:
1625 1625 pos = text.rfind('\n', None, pos)
1626 1626 if pos == -1:
1627 1627 pos = None
1628 1628 break
1629 1629 i += 1
1630 1630 if return_count:
1631 1631 return text[pos:], i
1632 1632 else:
1633 1633 return text[pos:]
1634 1634
1635 1635 def _get_last_lines_from_list(self, text_list, num_lines):
1636 1636 """ Return the list of text clipped to last specified lines.
1637 1637 """
1638 1638 ret = []
1639 1639 lines_pending = num_lines
1640 1640 for text in reversed(text_list):
1641 1641 text, lines_added = self._get_last_lines(text, lines_pending,
1642 1642 return_count=True)
1643 1643 ret.append(text)
1644 1644 lines_pending -= lines_added
1645 1645 if lines_pending <= 0:
1646 1646 break
1647 1647 return ret[::-1]
1648 1648
1649 1649 def _get_prompt_cursor(self):
1650 1650 """ Convenience method that returns a cursor for the prompt position.
1651 1651 """
1652 1652 cursor = self._control.textCursor()
1653 1653 cursor.setPosition(self._prompt_pos)
1654 1654 return cursor
1655 1655
1656 1656 def _get_selection_cursor(self, start, end):
1657 1657 """ Convenience method that returns a cursor with text selected between
1658 1658 the positions 'start' and 'end'.
1659 1659 """
1660 1660 cursor = self._control.textCursor()
1661 1661 cursor.setPosition(start)
1662 1662 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1663 1663 return cursor
1664 1664
1665 1665 def _get_word_start_cursor(self, position):
1666 1666 """ Find the start of the word to the left the given position. If a
1667 1667 sequence of non-word characters precedes the first word, skip over
1668 1668 them. (This emulates the behavior of bash, emacs, etc.)
1669 1669 """
1670 1670 document = self._control.document()
1671 1671 position -= 1
1672 1672 while position >= self._prompt_pos and \
1673 1673 not is_letter_or_number(document.characterAt(position)):
1674 1674 position -= 1
1675 1675 while position >= self._prompt_pos and \
1676 1676 is_letter_or_number(document.characterAt(position)):
1677 1677 position -= 1
1678 1678 cursor = self._control.textCursor()
1679 1679 cursor.setPosition(position + 1)
1680 1680 return cursor
1681 1681
1682 1682 def _get_word_end_cursor(self, position):
1683 1683 """ Find the end of the word to the right the given position. If a
1684 1684 sequence of non-word characters precedes the first word, skip over
1685 1685 them. (This emulates the behavior of bash, emacs, etc.)
1686 1686 """
1687 1687 document = self._control.document()
1688 1688 end = self._get_end_cursor().position()
1689 1689 while position < end and \
1690 1690 not is_letter_or_number(document.characterAt(position)):
1691 1691 position += 1
1692 1692 while position < end and \
1693 1693 is_letter_or_number(document.characterAt(position)):
1694 1694 position += 1
1695 1695 cursor = self._control.textCursor()
1696 1696 cursor.setPosition(position)
1697 1697 return cursor
1698 1698
1699 1699 def _insert_continuation_prompt(self, cursor):
1700 1700 """ Inserts new continuation prompt using the specified cursor.
1701 1701 """
1702 1702 if self._continuation_prompt_html is None:
1703 1703 self._insert_plain_text(cursor, self._continuation_prompt)
1704 1704 else:
1705 1705 self._continuation_prompt = self._insert_html_fetching_plain_text(
1706 1706 cursor, self._continuation_prompt_html)
1707 1707
1708 1708 def _insert_block(self, cursor, block_format=None):
1709 1709 """ Inserts an empty QTextBlock using the specified cursor.
1710 1710 """
1711 1711 if block_format is None:
1712 1712 block_format = QtGui.QTextBlockFormat()
1713 1713 cursor.insertBlock(block_format)
1714 1714
1715 1715 def _insert_html(self, cursor, html):
1716 1716 """ Inserts HTML using the specified cursor in such a way that future
1717 1717 formatting is unaffected.
1718 1718 """
1719 1719 cursor.beginEditBlock()
1720 1720 cursor.insertHtml(html)
1721 1721
1722 1722 # After inserting HTML, the text document "remembers" it's in "html
1723 1723 # mode", which means that subsequent calls adding plain text will result
1724 1724 # in unwanted formatting, lost tab characters, etc. The following code
1725 1725 # hacks around this behavior, which I consider to be a bug in Qt, by
1726 1726 # (crudely) resetting the document's style state.
1727 1727 cursor.movePosition(QtGui.QTextCursor.Left,
1728 1728 QtGui.QTextCursor.KeepAnchor)
1729 1729 if cursor.selection().toPlainText() == ' ':
1730 1730 cursor.removeSelectedText()
1731 1731 else:
1732 1732 cursor.movePosition(QtGui.QTextCursor.Right)
1733 1733 cursor.insertText(' ', QtGui.QTextCharFormat())
1734 1734 cursor.endEditBlock()
1735 1735
1736 1736 def _insert_html_fetching_plain_text(self, cursor, html):
1737 1737 """ Inserts HTML using the specified cursor, then returns its plain text
1738 1738 version.
1739 1739 """
1740 1740 cursor.beginEditBlock()
1741 1741 cursor.removeSelectedText()
1742 1742
1743 1743 start = cursor.position()
1744 1744 self._insert_html(cursor, html)
1745 1745 end = cursor.position()
1746 1746 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1747 1747 text = cursor.selection().toPlainText()
1748 1748
1749 1749 cursor.setPosition(end)
1750 1750 cursor.endEditBlock()
1751 1751 return text
1752 1752
1753 1753 def _insert_plain_text(self, cursor, text, flush=False):
1754 1754 """ Inserts plain text using the specified cursor, processing ANSI codes
1755 1755 if enabled.
1756 1756 """
1757 1757 # maximumBlockCount() can be different from self.buffer_size in
1758 1758 # case input prompt is active.
1759 1759 buffer_size = self._control.document().maximumBlockCount()
1760 1760
1761 if self._executing and not flush and \
1762 self._pending_text_flush_interval.isActive():
1761 if (self._executing and not flush and
1762 self._pending_text_flush_interval.isActive() and
1763 cursor.position() == self._get_end_cursor().position()):
1764 # Queue the text to insert in case it is being inserted at end
1763 1765 self._pending_insert_text.append(text)
1764 1766 if buffer_size > 0:
1765 1767 self._pending_insert_text = self._get_last_lines_from_list(
1766 1768 self._pending_insert_text, buffer_size)
1767 1769 return
1768 1770
1769 1771 if self._executing and not self._pending_text_flush_interval.isActive():
1770 1772 self._pending_text_flush_interval.start()
1771 1773
1772 1774 # Clip the text to last `buffer_size` lines.
1773 1775 if buffer_size > 0:
1774 1776 text = self._get_last_lines(text, buffer_size)
1775 1777
1776 1778 cursor.beginEditBlock()
1777 1779 if self.ansi_codes:
1778 1780 for substring in self._ansi_processor.split_string(text):
1779 1781 for act in self._ansi_processor.actions:
1780 1782
1781 1783 # Unlike real terminal emulators, we don't distinguish
1782 1784 # between the screen and the scrollback buffer. A screen
1783 1785 # erase request clears everything.
1784 1786 if act.action == 'erase' and act.area == 'screen':
1785 1787 cursor.select(QtGui.QTextCursor.Document)
1786 1788 cursor.removeSelectedText()
1787 1789
1788 1790 # Simulate a form feed by scrolling just past the last line.
1789 1791 elif act.action == 'scroll' and act.unit == 'page':
1790 1792 cursor.insertText('\n')
1791 1793 cursor.endEditBlock()
1792 1794 self._set_top_cursor(cursor)
1793 1795 cursor.joinPreviousEditBlock()
1794 1796 cursor.deletePreviousChar()
1795 1797
1796 1798 elif act.action == 'carriage-return':
1797 1799 cursor.movePosition(
1798 1800 cursor.StartOfLine, cursor.KeepAnchor)
1799 1801
1800 1802 elif act.action == 'beep':
1801 1803 QtGui.qApp.beep()
1802 1804
1803 1805 elif act.action == 'backspace':
1804 1806 if not cursor.atBlockStart():
1805 1807 cursor.movePosition(
1806 1808 cursor.PreviousCharacter, cursor.KeepAnchor)
1807 1809
1808 1810 elif act.action == 'newline':
1809 1811 cursor.movePosition(cursor.EndOfLine)
1810 1812
1811 1813 format = self._ansi_processor.get_format()
1812 1814
1813 1815 selection = cursor.selectedText()
1814 1816 if len(selection) == 0:
1815 1817 cursor.insertText(substring, format)
1816 1818 elif substring is not None:
1817 1819 # BS and CR are treated as a change in print
1818 1820 # position, rather than a backwards character
1819 1821 # deletion for output equivalence with (I)Python
1820 1822 # terminal.
1821 1823 if len(substring) >= len(selection):
1822 1824 cursor.insertText(substring, format)
1823 1825 else:
1824 1826 old_text = selection[len(substring):]
1825 1827 cursor.insertText(substring + old_text, format)
1826 1828 cursor.movePosition(cursor.PreviousCharacter,
1827 1829 cursor.KeepAnchor, len(old_text))
1828 1830 else:
1829 1831 cursor.insertText(text)
1830 1832 cursor.endEditBlock()
1831 1833
1832 1834 def _insert_plain_text_into_buffer(self, cursor, text):
1833 1835 """ Inserts text into the input buffer using the specified cursor (which
1834 1836 must be in the input buffer), ensuring that continuation prompts are
1835 1837 inserted as necessary.
1836 1838 """
1837 1839 lines = text.splitlines(True)
1838 1840 if lines:
1839 1841 cursor.beginEditBlock()
1840 1842 cursor.insertText(lines[0])
1841 1843 for line in lines[1:]:
1842 1844 if self._continuation_prompt_html is None:
1843 1845 cursor.insertText(self._continuation_prompt)
1844 1846 else:
1845 1847 self._continuation_prompt = \
1846 1848 self._insert_html_fetching_plain_text(
1847 1849 cursor, self._continuation_prompt_html)
1848 1850 cursor.insertText(line)
1849 1851 cursor.endEditBlock()
1850 1852
1851 1853 def _in_buffer(self, position=None):
1852 1854 """ Returns whether the current cursor (or, if specified, a position) is
1853 1855 inside the editing region.
1854 1856 """
1855 1857 cursor = self._control.textCursor()
1856 1858 if position is None:
1857 1859 position = cursor.position()
1858 1860 else:
1859 1861 cursor.setPosition(position)
1860 1862 line = cursor.blockNumber()
1861 1863 prompt_line = self._get_prompt_cursor().blockNumber()
1862 1864 if line == prompt_line:
1863 1865 return position >= self._prompt_pos
1864 1866 elif line > prompt_line:
1865 1867 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1866 1868 prompt_pos = cursor.position() + len(self._continuation_prompt)
1867 1869 return position >= prompt_pos
1868 1870 return False
1869 1871
1870 1872 def _keep_cursor_in_buffer(self):
1871 1873 """ Ensures that the cursor is inside the editing region. Returns
1872 1874 whether the cursor was moved.
1873 1875 """
1874 1876 moved = not self._in_buffer()
1875 1877 if moved:
1876 1878 cursor = self._control.textCursor()
1877 1879 cursor.movePosition(QtGui.QTextCursor.End)
1878 1880 self._control.setTextCursor(cursor)
1879 1881 return moved
1880 1882
1881 1883 def _keyboard_quit(self):
1882 1884 """ Cancels the current editing task ala Ctrl-G in Emacs.
1883 1885 """
1884 1886 if self._temp_buffer_filled :
1885 1887 self._cancel_completion()
1886 1888 self._clear_temporary_buffer()
1887 1889 else:
1888 1890 self.input_buffer = ''
1889 1891
1890 1892 def _page(self, text, html=False):
1891 1893 """ Displays text using the pager if it exceeds the height of the
1892 1894 viewport.
1893 1895
1894 1896 Parameters
1895 1897 ----------
1896 1898 html : bool, optional (default False)
1897 1899 If set, the text will be interpreted as HTML instead of plain text.
1898 1900 """
1899 1901 line_height = QtGui.QFontMetrics(self.font).height()
1900 1902 minlines = self._control.viewport().height() / line_height
1901 1903 if self.paging != 'none' and \
1902 1904 re.match("(?:[^\n]*\n){%i}" % minlines, text):
1903 1905 if self.paging == 'custom':
1904 1906 self.custom_page_requested.emit(text)
1905 1907 else:
1906 1908 self._page_control.clear()
1907 1909 cursor = self._page_control.textCursor()
1908 1910 if html:
1909 1911 self._insert_html(cursor, text)
1910 1912 else:
1911 1913 self._insert_plain_text(cursor, text)
1912 1914 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1913 1915
1914 1916 self._page_control.viewport().resize(self._control.size())
1915 1917 if self._splitter:
1916 1918 self._page_control.show()
1917 1919 self._page_control.setFocus()
1918 1920 else:
1919 1921 self.layout().setCurrentWidget(self._page_control)
1920 1922 elif html:
1921 1923 self._append_html(text)
1922 1924 else:
1923 1925 self._append_plain_text(text)
1924 1926
1925 1927 def _set_paging(self, paging):
1926 1928 """
1927 1929 Change the pager to `paging` style.
1928 1930
1929 1931 Parameters
1930 1932 ----------
1931 1933 paging : string
1932 1934 Either "hsplit", "vsplit", or "inside"
1933 1935 """
1934 1936 if self._splitter is None:
1935 1937 raise NotImplementedError("""can only switch if --paging=hsplit or
1936 1938 --paging=vsplit is used.""")
1937 1939 if paging == 'hsplit':
1938 1940 self._splitter.setOrientation(QtCore.Qt.Horizontal)
1939 1941 elif paging == 'vsplit':
1940 1942 self._splitter.setOrientation(QtCore.Qt.Vertical)
1941 1943 elif paging == 'inside':
1942 1944 raise NotImplementedError("""switching to 'inside' paging not
1943 1945 supported yet.""")
1944 1946 else:
1945 1947 raise ValueError("unknown paging method '%s'" % paging)
1946 1948 self.paging = paging
1947 1949
1948 1950 def _prompt_finished(self):
1949 1951 """ Called immediately after a prompt is finished, i.e. when some input
1950 1952 will be processed and a new prompt displayed.
1951 1953 """
1952 1954 self._control.setReadOnly(True)
1953 1955 self._prompt_finished_hook()
1954 1956
1955 1957 def _prompt_started(self):
1956 1958 """ Called immediately after a new prompt is displayed.
1957 1959 """
1958 1960 # Temporarily disable the maximum block count to permit undo/redo and
1959 1961 # to ensure that the prompt position does not change due to truncation.
1960 1962 self._control.document().setMaximumBlockCount(0)
1961 1963 self._control.setUndoRedoEnabled(True)
1962 1964
1963 1965 # Work around bug in QPlainTextEdit: input method is not re-enabled
1964 1966 # when read-only is disabled.
1965 1967 self._control.setReadOnly(False)
1966 1968 self._control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1967 1969
1968 1970 if not self._reading:
1969 1971 self._executing = False
1970 1972 self._prompt_started_hook()
1971 1973
1972 1974 # If the input buffer has changed while executing, load it.
1973 1975 if self._input_buffer_pending:
1974 1976 self.input_buffer = self._input_buffer_pending
1975 1977 self._input_buffer_pending = ''
1976 1978
1977 1979 self._control.moveCursor(QtGui.QTextCursor.End)
1978 1980
1979 1981 def _readline(self, prompt='', callback=None):
1980 1982 """ Reads one line of input from the user.
1981 1983
1982 1984 Parameters
1983 1985 ----------
1984 1986 prompt : str, optional
1985 1987 The prompt to print before reading the line.
1986 1988
1987 1989 callback : callable, optional
1988 1990 A callback to execute with the read line. If not specified, input is
1989 1991 read *synchronously* and this method does not return until it has
1990 1992 been read.
1991 1993
1992 1994 Returns
1993 1995 -------
1994 1996 If a callback is specified, returns nothing. Otherwise, returns the
1995 1997 input string with the trailing newline stripped.
1996 1998 """
1997 1999 if self._reading:
1998 2000 raise RuntimeError('Cannot read a line. Widget is already reading.')
1999 2001
2000 2002 if not callback and not self.isVisible():
2001 2003 # If the user cannot see the widget, this function cannot return.
2002 2004 raise RuntimeError('Cannot synchronously read a line if the widget '
2003 2005 'is not visible!')
2004 2006
2005 2007 self._reading = True
2006 2008 self._show_prompt(prompt, newline=False)
2007 2009
2008 2010 if callback is None:
2009 2011 self._reading_callback = None
2010 2012 while self._reading:
2011 2013 QtCore.QCoreApplication.processEvents()
2012 2014 return self._get_input_buffer(force=True).rstrip('\n')
2013 2015
2014 2016 else:
2015 2017 self._reading_callback = lambda: \
2016 2018 callback(self._get_input_buffer(force=True).rstrip('\n'))
2017 2019
2018 2020 def _set_continuation_prompt(self, prompt, html=False):
2019 2021 """ Sets the continuation prompt.
2020 2022
2021 2023 Parameters
2022 2024 ----------
2023 2025 prompt : str
2024 2026 The prompt to show when more input is needed.
2025 2027
2026 2028 html : bool, optional (default False)
2027 2029 If set, the prompt will be inserted as formatted HTML. Otherwise,
2028 2030 the prompt will be treated as plain text, though ANSI color codes
2029 2031 will be handled.
2030 2032 """
2031 2033 if html:
2032 2034 self._continuation_prompt_html = prompt
2033 2035 else:
2034 2036 self._continuation_prompt = prompt
2035 2037 self._continuation_prompt_html = None
2036 2038
2037 2039 def _set_cursor(self, cursor):
2038 2040 """ Convenience method to set the current cursor.
2039 2041 """
2040 2042 self._control.setTextCursor(cursor)
2041 2043
2042 2044 def _set_top_cursor(self, cursor):
2043 2045 """ Scrolls the viewport so that the specified cursor is at the top.
2044 2046 """
2045 2047 scrollbar = self._control.verticalScrollBar()
2046 2048 scrollbar.setValue(scrollbar.maximum())
2047 2049 original_cursor = self._control.textCursor()
2048 2050 self._control.setTextCursor(cursor)
2049 2051 self._control.ensureCursorVisible()
2050 2052 self._control.setTextCursor(original_cursor)
2051 2053
2052 2054 def _show_prompt(self, prompt=None, html=False, newline=True):
2053 2055 """ Writes a new prompt at the end of the buffer.
2054 2056
2055 2057 Parameters
2056 2058 ----------
2057 2059 prompt : str, optional
2058 2060 The prompt to show. If not specified, the previous prompt is used.
2059 2061
2060 2062 html : bool, optional (default False)
2061 2063 Only relevant when a prompt is specified. If set, the prompt will
2062 2064 be inserted as formatted HTML. Otherwise, the prompt will be treated
2063 2065 as plain text, though ANSI color codes will be handled.
2064 2066
2065 2067 newline : bool, optional (default True)
2066 2068 If set, a new line will be written before showing the prompt if
2067 2069 there is not already a newline at the end of the buffer.
2068 2070 """
2069 2071 # Save the current end position to support _append*(before_prompt=True).
2070 2072 self._flush_pending_stream()
2071 2073 cursor = self._get_end_cursor()
2072 2074 self._append_before_prompt_pos = cursor.position()
2073 2075
2074 2076 # Insert a preliminary newline, if necessary.
2075 2077 if newline and cursor.position() > 0:
2076 2078 cursor.movePosition(QtGui.QTextCursor.Left,
2077 2079 QtGui.QTextCursor.KeepAnchor)
2078 2080 if cursor.selection().toPlainText() != '\n':
2079 2081 self._append_block()
2080 2082 self._append_before_prompt_pos += 1
2081 2083
2082 2084 # Write the prompt.
2083 2085 self._append_plain_text(self._prompt_sep)
2084 2086 if prompt is None:
2085 2087 if self._prompt_html is None:
2086 2088 self._append_plain_text(self._prompt)
2087 2089 else:
2088 2090 self._append_html(self._prompt_html)
2089 2091 else:
2090 2092 if html:
2091 2093 self._prompt = self._append_html_fetching_plain_text(prompt)
2092 2094 self._prompt_html = prompt
2093 2095 else:
2094 2096 self._append_plain_text(prompt)
2095 2097 self._prompt = prompt
2096 2098 self._prompt_html = None
2097 2099
2100 self._flush_pending_stream()
2098 2101 self._prompt_pos = self._get_end_cursor().position()
2099 2102 self._prompt_started()
2100 2103
2101 2104 #------ Signal handlers ----------------------------------------------------
2102 2105
2103 2106 def _adjust_scrollbars(self):
2104 2107 """ Expands the vertical scrollbar beyond the range set by Qt.
2105 2108 """
2106 2109 # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
2107 2110 # and qtextedit.cpp.
2108 2111 document = self._control.document()
2109 2112 scrollbar = self._control.verticalScrollBar()
2110 2113 viewport_height = self._control.viewport().height()
2111 2114 if isinstance(self._control, QtGui.QPlainTextEdit):
2112 2115 maximum = max(0, document.lineCount() - 1)
2113 2116 step = viewport_height / self._control.fontMetrics().lineSpacing()
2114 2117 else:
2115 2118 # QTextEdit does not do line-based layout and blocks will not in
2116 2119 # general have the same height. Therefore it does not make sense to
2117 2120 # attempt to scroll in line height increments.
2118 2121 maximum = document.size().height()
2119 2122 step = viewport_height
2120 2123 diff = maximum - scrollbar.maximum()
2121 2124 scrollbar.setRange(0, maximum)
2122 2125 scrollbar.setPageStep(step)
2123 2126
2124 2127 # Compensate for undesirable scrolling that occurs automatically due to
2125 2128 # maximumBlockCount() text truncation.
2126 2129 if diff < 0 and document.blockCount() == document.maximumBlockCount():
2127 2130 scrollbar.setValue(scrollbar.value() + diff)
2128 2131
2129 2132 def _custom_context_menu_requested(self, pos):
2130 2133 """ Shows a context menu at the given QPoint (in widget coordinates).
2131 2134 """
2132 2135 menu = self._context_menu_make(pos)
2133 2136 menu.exec_(self._control.mapToGlobal(pos))
General Comments 0
You need to be logged in to leave comments. Login now