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