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