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