##// END OF EJS Templates
Backport PR #2101: fix completer deletting newline...
MinRK -
Show More
@@ -1,1921 +1,1922 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 861 def _append_html(self, html, before_prompt=False):
862 862 """ Appends HTML at the end of the console buffer.
863 863 """
864 864 self._append_custom(self._insert_html, html, before_prompt)
865 865
866 866 def _append_html_fetching_plain_text(self, html, before_prompt=False):
867 867 """ Appends HTML, then returns the plain text version of it.
868 868 """
869 869 return self._append_custom(self._insert_html_fetching_plain_text,
870 870 html, before_prompt)
871 871
872 872 def _append_plain_text(self, text, before_prompt=False):
873 873 """ Appends plain text, processing ANSI codes if enabled.
874 874 """
875 875 self._append_custom(self._insert_plain_text, text, before_prompt)
876 876
877 877 def _cancel_completion(self):
878 878 """ If text completion is progress, cancel it.
879 879 """
880 880 self._completion_widget.cancel_completion()
881 881
882 882 def _clear_temporary_buffer(self):
883 883 """ Clears the "temporary text" buffer, i.e. all the text following
884 884 the prompt region.
885 885 """
886 886 # Select and remove all text below the input buffer.
887 _temp_buffer_filled = False
888 887 cursor = self._get_prompt_cursor()
889 888 prompt = self._continuation_prompt.lstrip()
889 if(self._temp_buffer_filled):
890 self._temp_buffer_filled = False
890 891 while cursor.movePosition(QtGui.QTextCursor.NextBlock):
891 892 temp_cursor = QtGui.QTextCursor(cursor)
892 893 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
893 894 text = temp_cursor.selection().toPlainText().lstrip()
894 895 if not text.startswith(prompt):
895 896 break
896 897 else:
897 898 # We've reached the end of the input buffer and no text follows.
898 899 return
899 900 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
900 901 cursor.movePosition(QtGui.QTextCursor.End,
901 902 QtGui.QTextCursor.KeepAnchor)
902 903 cursor.removeSelectedText()
903 904
904 905 # After doing this, we have no choice but to clear the undo/redo
905 906 # history. Otherwise, the text is not "temporary" at all, because it
906 907 # can be recalled with undo/redo. Unfortunately, Qt does not expose
907 908 # fine-grained control to the undo/redo system.
908 909 if self._control.isUndoRedoEnabled():
909 910 self._control.setUndoRedoEnabled(False)
910 911 self._control.setUndoRedoEnabled(True)
911 912
912 913 def _complete_with_items(self, cursor, items):
913 914 """ Performs completion with 'items' at the specified cursor location.
914 915 """
915 916 self._cancel_completion()
916 917
917 918 if len(items) == 1:
918 919 cursor.setPosition(self._control.textCursor().position(),
919 920 QtGui.QTextCursor.KeepAnchor)
920 921 cursor.insertText(items[0])
921 922
922 923 elif len(items) > 1:
923 924 current_pos = self._control.textCursor().position()
924 925 prefix = commonprefix(items)
925 926 if prefix:
926 927 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
927 928 cursor.insertText(prefix)
928 929 current_pos = cursor.position()
929 930
930 931 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
931 932 self._completion_widget.show_items(cursor, items)
932 933
933 934
934 935 def _fill_temporary_buffer(self, cursor, text, html=False):
935 936 """fill the area below the active editting zone with text"""
936 937
937 938 current_pos = self._control.textCursor().position()
938 939
939 940 cursor.beginEditBlock()
940 941 self._append_plain_text('\n')
941 942 self._page(text, html=html)
942 943 cursor.endEditBlock()
943 944
944 945 cursor.setPosition(current_pos)
945 946 self._control.moveCursor(QtGui.QTextCursor.End)
946 947 self._control.setTextCursor(cursor)
947 948
948 _temp_buffer_filled = True
949 self._temp_buffer_filled = True
949 950
950 951
951 952 def _context_menu_make(self, pos):
952 953 """ Creates a context menu for the given QPoint (in widget coordinates).
953 954 """
954 955 menu = QtGui.QMenu(self)
955 956
956 957 self.cut_action = menu.addAction('Cut', self.cut)
957 958 self.cut_action.setEnabled(self.can_cut())
958 959 self.cut_action.setShortcut(QtGui.QKeySequence.Cut)
959 960
960 961 self.copy_action = menu.addAction('Copy', self.copy)
961 962 self.copy_action.setEnabled(self.can_copy())
962 963 self.copy_action.setShortcut(QtGui.QKeySequence.Copy)
963 964
964 965 self.paste_action = menu.addAction('Paste', self.paste)
965 966 self.paste_action.setEnabled(self.can_paste())
966 967 self.paste_action.setShortcut(QtGui.QKeySequence.Paste)
967 968
968 969 menu.addSeparator()
969 970 menu.addAction(self.select_all_action)
970 971
971 972 menu.addSeparator()
972 973 menu.addAction(self.export_action)
973 974 menu.addAction(self.print_action)
974 975
975 976 return menu
976 977
977 978 def _control_key_down(self, modifiers, include_command=False):
978 979 """ Given a KeyboardModifiers flags object, return whether the Control
979 980 key is down.
980 981
981 982 Parameters:
982 983 -----------
983 984 include_command : bool, optional (default True)
984 985 Whether to treat the Command key as a (mutually exclusive) synonym
985 986 for Control when in Mac OS.
986 987 """
987 988 # Note that on Mac OS, ControlModifier corresponds to the Command key
988 989 # while MetaModifier corresponds to the Control key.
989 990 if sys.platform == 'darwin':
990 991 down = include_command and (modifiers & QtCore.Qt.ControlModifier)
991 992 return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
992 993 else:
993 994 return bool(modifiers & QtCore.Qt.ControlModifier)
994 995
995 996 def _create_control(self):
996 997 """ Creates and connects the underlying text widget.
997 998 """
998 999 # Create the underlying control.
999 1000 if self.custom_control:
1000 1001 control = self.custom_control()
1001 1002 elif self.kind == 'plain':
1002 1003 control = QtGui.QPlainTextEdit()
1003 1004 elif self.kind == 'rich':
1004 1005 control = QtGui.QTextEdit()
1005 1006 control.setAcceptRichText(False)
1006 1007
1007 1008 # Install event filters. The filter on the viewport is needed for
1008 1009 # mouse events and drag events.
1009 1010 control.installEventFilter(self)
1010 1011 control.viewport().installEventFilter(self)
1011 1012
1012 1013 # Connect signals.
1013 1014 control.customContextMenuRequested.connect(
1014 1015 self._custom_context_menu_requested)
1015 1016 control.copyAvailable.connect(self.copy_available)
1016 1017 control.redoAvailable.connect(self.redo_available)
1017 1018 control.undoAvailable.connect(self.undo_available)
1018 1019
1019 1020 # Hijack the document size change signal to prevent Qt from adjusting
1020 1021 # the viewport's scrollbar. We are relying on an implementation detail
1021 1022 # of Q(Plain)TextEdit here, which is potentially dangerous, but without
1022 1023 # this functionality we cannot create a nice terminal interface.
1023 1024 layout = control.document().documentLayout()
1024 1025 layout.documentSizeChanged.disconnect()
1025 1026 layout.documentSizeChanged.connect(self._adjust_scrollbars)
1026 1027
1027 1028 # Configure the control.
1028 1029 control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1029 1030 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
1030 1031 control.setReadOnly(True)
1031 1032 control.setUndoRedoEnabled(False)
1032 1033 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
1033 1034 return control
1034 1035
1035 1036 def _create_page_control(self):
1036 1037 """ Creates and connects the underlying paging widget.
1037 1038 """
1038 1039 if self.custom_page_control:
1039 1040 control = self.custom_page_control()
1040 1041 elif self.kind == 'plain':
1041 1042 control = QtGui.QPlainTextEdit()
1042 1043 elif self.kind == 'rich':
1043 1044 control = QtGui.QTextEdit()
1044 1045 control.installEventFilter(self)
1045 1046 viewport = control.viewport()
1046 1047 viewport.installEventFilter(self)
1047 1048 control.setReadOnly(True)
1048 1049 control.setUndoRedoEnabled(False)
1049 1050 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
1050 1051 return control
1051 1052
1052 1053 def _event_filter_console_keypress(self, event):
1053 1054 """ Filter key events for the underlying text widget to create a
1054 1055 console-like interface.
1055 1056 """
1056 1057 intercepted = False
1057 1058 cursor = self._control.textCursor()
1058 1059 position = cursor.position()
1059 1060 key = event.key()
1060 1061 ctrl_down = self._control_key_down(event.modifiers())
1061 1062 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1062 1063 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
1063 1064
1064 1065 #------ Special sequences ----------------------------------------------
1065 1066
1066 1067 if event.matches(QtGui.QKeySequence.Copy):
1067 1068 self.copy()
1068 1069 intercepted = True
1069 1070
1070 1071 elif event.matches(QtGui.QKeySequence.Cut):
1071 1072 self.cut()
1072 1073 intercepted = True
1073 1074
1074 1075 elif event.matches(QtGui.QKeySequence.Paste):
1075 1076 self.paste()
1076 1077 intercepted = True
1077 1078
1078 1079 #------ Special modifier logic -----------------------------------------
1079 1080
1080 1081 elif key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
1081 1082 intercepted = True
1082 1083
1083 1084 # Special handling when tab completing in text mode.
1084 1085 self._cancel_completion()
1085 1086
1086 1087 if self._in_buffer(position):
1087 1088 # Special handling when a reading a line of raw input.
1088 1089 if self._reading:
1089 1090 self._append_plain_text('\n')
1090 1091 self._reading = False
1091 1092 if self._reading_callback:
1092 1093 self._reading_callback()
1093 1094
1094 1095 # If the input buffer is a single line or there is only
1095 1096 # whitespace after the cursor, execute. Otherwise, split the
1096 1097 # line with a continuation prompt.
1097 1098 elif not self._executing:
1098 1099 cursor.movePosition(QtGui.QTextCursor.End,
1099 1100 QtGui.QTextCursor.KeepAnchor)
1100 1101 at_end = len(cursor.selectedText().strip()) == 0
1101 1102 single_line = (self._get_end_cursor().blockNumber() ==
1102 1103 self._get_prompt_cursor().blockNumber())
1103 1104 if (at_end or shift_down or single_line) and not ctrl_down:
1104 1105 self.execute(interactive = not shift_down)
1105 1106 else:
1106 1107 # Do this inside an edit block for clean undo/redo.
1107 1108 cursor.beginEditBlock()
1108 1109 cursor.setPosition(position)
1109 1110 cursor.insertText('\n')
1110 1111 self._insert_continuation_prompt(cursor)
1111 1112 cursor.endEditBlock()
1112 1113
1113 1114 # Ensure that the whole input buffer is visible.
1114 1115 # FIXME: This will not be usable if the input buffer is
1115 1116 # taller than the console widget.
1116 1117 self._control.moveCursor(QtGui.QTextCursor.End)
1117 1118 self._control.setTextCursor(cursor)
1118 1119
1119 1120 #------ Control/Cmd modifier -------------------------------------------
1120 1121
1121 1122 elif ctrl_down:
1122 1123 if key == QtCore.Qt.Key_G:
1123 1124 self._keyboard_quit()
1124 1125 intercepted = True
1125 1126
1126 1127 elif key == QtCore.Qt.Key_K:
1127 1128 if self._in_buffer(position):
1128 1129 cursor.clearSelection()
1129 1130 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
1130 1131 QtGui.QTextCursor.KeepAnchor)
1131 1132 if not cursor.hasSelection():
1132 1133 # Line deletion (remove continuation prompt)
1133 1134 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1134 1135 QtGui.QTextCursor.KeepAnchor)
1135 1136 cursor.movePosition(QtGui.QTextCursor.Right,
1136 1137 QtGui.QTextCursor.KeepAnchor,
1137 1138 len(self._continuation_prompt))
1138 1139 self._kill_ring.kill_cursor(cursor)
1139 1140 self._set_cursor(cursor)
1140 1141 intercepted = True
1141 1142
1142 1143 elif key == QtCore.Qt.Key_L:
1143 1144 self.prompt_to_top()
1144 1145 intercepted = True
1145 1146
1146 1147 elif key == QtCore.Qt.Key_O:
1147 1148 if self._page_control and self._page_control.isVisible():
1148 1149 self._page_control.setFocus()
1149 1150 intercepted = True
1150 1151
1151 1152 elif key == QtCore.Qt.Key_U:
1152 1153 if self._in_buffer(position):
1153 1154 cursor.clearSelection()
1154 1155 start_line = cursor.blockNumber()
1155 1156 if start_line == self._get_prompt_cursor().blockNumber():
1156 1157 offset = len(self._prompt)
1157 1158 else:
1158 1159 offset = len(self._continuation_prompt)
1159 1160 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1160 1161 QtGui.QTextCursor.KeepAnchor)
1161 1162 cursor.movePosition(QtGui.QTextCursor.Right,
1162 1163 QtGui.QTextCursor.KeepAnchor, offset)
1163 1164 self._kill_ring.kill_cursor(cursor)
1164 1165 self._set_cursor(cursor)
1165 1166 intercepted = True
1166 1167
1167 1168 elif key == QtCore.Qt.Key_Y:
1168 1169 self._keep_cursor_in_buffer()
1169 1170 self._kill_ring.yank()
1170 1171 intercepted = True
1171 1172
1172 1173 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
1173 1174 if key == QtCore.Qt.Key_Backspace:
1174 1175 cursor = self._get_word_start_cursor(position)
1175 1176 else: # key == QtCore.Qt.Key_Delete
1176 1177 cursor = self._get_word_end_cursor(position)
1177 1178 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1178 1179 self._kill_ring.kill_cursor(cursor)
1179 1180 intercepted = True
1180 1181
1181 1182 elif key == QtCore.Qt.Key_D:
1182 1183 if len(self.input_buffer) == 0:
1183 1184 self.exit_requested.emit(self)
1184 1185 else:
1185 1186 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1186 1187 QtCore.Qt.Key_Delete,
1187 1188 QtCore.Qt.NoModifier)
1188 1189 QtGui.qApp.sendEvent(self._control, new_event)
1189 1190 intercepted = True
1190 1191
1191 1192 #------ Alt modifier ---------------------------------------------------
1192 1193
1193 1194 elif alt_down:
1194 1195 if key == QtCore.Qt.Key_B:
1195 1196 self._set_cursor(self._get_word_start_cursor(position))
1196 1197 intercepted = True
1197 1198
1198 1199 elif key == QtCore.Qt.Key_F:
1199 1200 self._set_cursor(self._get_word_end_cursor(position))
1200 1201 intercepted = True
1201 1202
1202 1203 elif key == QtCore.Qt.Key_Y:
1203 1204 self._kill_ring.rotate()
1204 1205 intercepted = True
1205 1206
1206 1207 elif key == QtCore.Qt.Key_Backspace:
1207 1208 cursor = self._get_word_start_cursor(position)
1208 1209 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1209 1210 self._kill_ring.kill_cursor(cursor)
1210 1211 intercepted = True
1211 1212
1212 1213 elif key == QtCore.Qt.Key_D:
1213 1214 cursor = self._get_word_end_cursor(position)
1214 1215 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1215 1216 self._kill_ring.kill_cursor(cursor)
1216 1217 intercepted = True
1217 1218
1218 1219 elif key == QtCore.Qt.Key_Delete:
1219 1220 intercepted = True
1220 1221
1221 1222 elif key == QtCore.Qt.Key_Greater:
1222 1223 self._control.moveCursor(QtGui.QTextCursor.End)
1223 1224 intercepted = True
1224 1225
1225 1226 elif key == QtCore.Qt.Key_Less:
1226 1227 self._control.setTextCursor(self._get_prompt_cursor())
1227 1228 intercepted = True
1228 1229
1229 1230 #------ No modifiers ---------------------------------------------------
1230 1231
1231 1232 else:
1232 1233 if shift_down:
1233 1234 anchormode = QtGui.QTextCursor.KeepAnchor
1234 1235 else:
1235 1236 anchormode = QtGui.QTextCursor.MoveAnchor
1236 1237
1237 1238 if key == QtCore.Qt.Key_Escape:
1238 1239 self._keyboard_quit()
1239 1240 intercepted = True
1240 1241
1241 1242 elif key == QtCore.Qt.Key_Up:
1242 1243 if self._reading or not self._up_pressed(shift_down):
1243 1244 intercepted = True
1244 1245 else:
1245 1246 prompt_line = self._get_prompt_cursor().blockNumber()
1246 1247 intercepted = cursor.blockNumber() <= prompt_line
1247 1248
1248 1249 elif key == QtCore.Qt.Key_Down:
1249 1250 if self._reading or not self._down_pressed(shift_down):
1250 1251 intercepted = True
1251 1252 else:
1252 1253 end_line = self._get_end_cursor().blockNumber()
1253 1254 intercepted = cursor.blockNumber() == end_line
1254 1255
1255 1256 elif key == QtCore.Qt.Key_Tab:
1256 1257 if not self._reading:
1257 1258 if self._tab_pressed():
1258 1259 # real tab-key, insert four spaces
1259 1260 cursor.insertText(' '*4)
1260 1261 intercepted = True
1261 1262
1262 1263 elif key == QtCore.Qt.Key_Left:
1263 1264
1264 1265 # Move to the previous line
1265 1266 line, col = cursor.blockNumber(), cursor.columnNumber()
1266 1267 if line > self._get_prompt_cursor().blockNumber() and \
1267 1268 col == len(self._continuation_prompt):
1268 1269 self._control.moveCursor(QtGui.QTextCursor.PreviousBlock,
1269 1270 mode=anchormode)
1270 1271 self._control.moveCursor(QtGui.QTextCursor.EndOfBlock,
1271 1272 mode=anchormode)
1272 1273 intercepted = True
1273 1274
1274 1275 # Regular left movement
1275 1276 else:
1276 1277 intercepted = not self._in_buffer(position - 1)
1277 1278
1278 1279 elif key == QtCore.Qt.Key_Right:
1279 1280 original_block_number = cursor.blockNumber()
1280 1281 cursor.movePosition(QtGui.QTextCursor.Right,
1281 1282 mode=anchormode)
1282 1283 if cursor.blockNumber() != original_block_number:
1283 1284 cursor.movePosition(QtGui.QTextCursor.Right,
1284 1285 n=len(self._continuation_prompt),
1285 1286 mode=anchormode)
1286 1287 self._set_cursor(cursor)
1287 1288 intercepted = True
1288 1289
1289 1290 elif key == QtCore.Qt.Key_Home:
1290 1291 start_line = cursor.blockNumber()
1291 1292 if start_line == self._get_prompt_cursor().blockNumber():
1292 1293 start_pos = self._prompt_pos
1293 1294 else:
1294 1295 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1295 1296 QtGui.QTextCursor.KeepAnchor)
1296 1297 start_pos = cursor.position()
1297 1298 start_pos += len(self._continuation_prompt)
1298 1299 cursor.setPosition(position)
1299 1300 if shift_down and self._in_buffer(position):
1300 1301 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
1301 1302 else:
1302 1303 cursor.setPosition(start_pos)
1303 1304 self._set_cursor(cursor)
1304 1305 intercepted = True
1305 1306
1306 1307 elif key == QtCore.Qt.Key_Backspace:
1307 1308
1308 1309 # Line deletion (remove continuation prompt)
1309 1310 line, col = cursor.blockNumber(), cursor.columnNumber()
1310 1311 if not self._reading and \
1311 1312 col == len(self._continuation_prompt) and \
1312 1313 line > self._get_prompt_cursor().blockNumber():
1313 1314 cursor.beginEditBlock()
1314 1315 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1315 1316 QtGui.QTextCursor.KeepAnchor)
1316 1317 cursor.removeSelectedText()
1317 1318 cursor.deletePreviousChar()
1318 1319 cursor.endEditBlock()
1319 1320 intercepted = True
1320 1321
1321 1322 # Regular backwards deletion
1322 1323 else:
1323 1324 anchor = cursor.anchor()
1324 1325 if anchor == position:
1325 1326 intercepted = not self._in_buffer(position - 1)
1326 1327 else:
1327 1328 intercepted = not self._in_buffer(min(anchor, position))
1328 1329
1329 1330 elif key == QtCore.Qt.Key_Delete:
1330 1331
1331 1332 # Line deletion (remove continuation prompt)
1332 1333 if not self._reading and self._in_buffer(position) and \
1333 1334 cursor.atBlockEnd() and not cursor.hasSelection():
1334 1335 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1335 1336 QtGui.QTextCursor.KeepAnchor)
1336 1337 cursor.movePosition(QtGui.QTextCursor.Right,
1337 1338 QtGui.QTextCursor.KeepAnchor,
1338 1339 len(self._continuation_prompt))
1339 1340 cursor.removeSelectedText()
1340 1341 intercepted = True
1341 1342
1342 1343 # Regular forwards deletion:
1343 1344 else:
1344 1345 anchor = cursor.anchor()
1345 1346 intercepted = (not self._in_buffer(anchor) or
1346 1347 not self._in_buffer(position))
1347 1348
1348 1349 # Don't move the cursor if Control/Cmd is pressed to allow copy-paste
1349 1350 # using the keyboard in any part of the buffer. Also, permit scrolling
1350 1351 # with Page Up/Down keys. Finally, if we're executing, don't move the
1351 1352 # cursor (if even this made sense, we can't guarantee that the prompt
1352 1353 # position is still valid due to text truncation).
1353 1354 if not (self._control_key_down(event.modifiers(), include_command=True)
1354 1355 or key in (QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown)
1355 1356 or (self._executing and not self._reading)):
1356 1357 self._keep_cursor_in_buffer()
1357 1358
1358 1359 return intercepted
1359 1360
1360 1361 def _event_filter_page_keypress(self, event):
1361 1362 """ Filter key events for the paging widget to create console-like
1362 1363 interface.
1363 1364 """
1364 1365 key = event.key()
1365 1366 ctrl_down = self._control_key_down(event.modifiers())
1366 1367 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1367 1368
1368 1369 if ctrl_down:
1369 1370 if key == QtCore.Qt.Key_O:
1370 1371 self._control.setFocus()
1371 1372 intercept = True
1372 1373
1373 1374 elif alt_down:
1374 1375 if key == QtCore.Qt.Key_Greater:
1375 1376 self._page_control.moveCursor(QtGui.QTextCursor.End)
1376 1377 intercepted = True
1377 1378
1378 1379 elif key == QtCore.Qt.Key_Less:
1379 1380 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1380 1381 intercepted = True
1381 1382
1382 1383 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
1383 1384 if self._splitter:
1384 1385 self._page_control.hide()
1385 1386 self._control.setFocus()
1386 1387 else:
1387 1388 self.layout().setCurrentWidget(self._control)
1388 1389 return True
1389 1390
1390 1391 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return,
1391 1392 QtCore.Qt.Key_Tab):
1392 1393 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1393 1394 QtCore.Qt.Key_PageDown,
1394 1395 QtCore.Qt.NoModifier)
1395 1396 QtGui.qApp.sendEvent(self._page_control, new_event)
1396 1397 return True
1397 1398
1398 1399 elif key == QtCore.Qt.Key_Backspace:
1399 1400 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1400 1401 QtCore.Qt.Key_PageUp,
1401 1402 QtCore.Qt.NoModifier)
1402 1403 QtGui.qApp.sendEvent(self._page_control, new_event)
1403 1404 return True
1404 1405
1405 1406 return False
1406 1407
1407 1408 def _format_as_columns(self, items, separator=' '):
1408 1409 """ Transform a list of strings into a single string with columns.
1409 1410
1410 1411 Parameters
1411 1412 ----------
1412 1413 items : sequence of strings
1413 1414 The strings to process.
1414 1415
1415 1416 separator : str, optional [default is two spaces]
1416 1417 The string that separates columns.
1417 1418
1418 1419 Returns
1419 1420 -------
1420 1421 The formatted string.
1421 1422 """
1422 1423 # Calculate the number of characters available.
1423 1424 width = self._control.viewport().width()
1424 1425 char_width = QtGui.QFontMetrics(self.font).width(' ')
1425 1426 displaywidth = max(10, (width / char_width) - 1)
1426 1427
1427 1428 return columnize(items, separator, displaywidth)
1428 1429
1429 1430 def _get_block_plain_text(self, block):
1430 1431 """ Given a QTextBlock, return its unformatted text.
1431 1432 """
1432 1433 cursor = QtGui.QTextCursor(block)
1433 1434 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1434 1435 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1435 1436 QtGui.QTextCursor.KeepAnchor)
1436 1437 return cursor.selection().toPlainText()
1437 1438
1438 1439 def _get_cursor(self):
1439 1440 """ Convenience method that returns a cursor for the current position.
1440 1441 """
1441 1442 return self._control.textCursor()
1442 1443
1443 1444 def _get_end_cursor(self):
1444 1445 """ Convenience method that returns a cursor for the last character.
1445 1446 """
1446 1447 cursor = self._control.textCursor()
1447 1448 cursor.movePosition(QtGui.QTextCursor.End)
1448 1449 return cursor
1449 1450
1450 1451 def _get_input_buffer_cursor_column(self):
1451 1452 """ Returns the column of the cursor in the input buffer, excluding the
1452 1453 contribution by the prompt, or -1 if there is no such column.
1453 1454 """
1454 1455 prompt = self._get_input_buffer_cursor_prompt()
1455 1456 if prompt is None:
1456 1457 return -1
1457 1458 else:
1458 1459 cursor = self._control.textCursor()
1459 1460 return cursor.columnNumber() - len(prompt)
1460 1461
1461 1462 def _get_input_buffer_cursor_line(self):
1462 1463 """ Returns the text of the line of the input buffer that contains the
1463 1464 cursor, or None if there is no such line.
1464 1465 """
1465 1466 prompt = self._get_input_buffer_cursor_prompt()
1466 1467 if prompt is None:
1467 1468 return None
1468 1469 else:
1469 1470 cursor = self._control.textCursor()
1470 1471 text = self._get_block_plain_text(cursor.block())
1471 1472 return text[len(prompt):]
1472 1473
1473 1474 def _get_input_buffer_cursor_prompt(self):
1474 1475 """ Returns the (plain text) prompt for line of the input buffer that
1475 1476 contains the cursor, or None if there is no such line.
1476 1477 """
1477 1478 if self._executing:
1478 1479 return None
1479 1480 cursor = self._control.textCursor()
1480 1481 if cursor.position() >= self._prompt_pos:
1481 1482 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1482 1483 return self._prompt
1483 1484 else:
1484 1485 return self._continuation_prompt
1485 1486 else:
1486 1487 return None
1487 1488
1488 1489 def _get_prompt_cursor(self):
1489 1490 """ Convenience method that returns a cursor for the prompt position.
1490 1491 """
1491 1492 cursor = self._control.textCursor()
1492 1493 cursor.setPosition(self._prompt_pos)
1493 1494 return cursor
1494 1495
1495 1496 def _get_selection_cursor(self, start, end):
1496 1497 """ Convenience method that returns a cursor with text selected between
1497 1498 the positions 'start' and 'end'.
1498 1499 """
1499 1500 cursor = self._control.textCursor()
1500 1501 cursor.setPosition(start)
1501 1502 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1502 1503 return cursor
1503 1504
1504 1505 def _get_word_start_cursor(self, position):
1505 1506 """ Find the start of the word to the left the given position. If a
1506 1507 sequence of non-word characters precedes the first word, skip over
1507 1508 them. (This emulates the behavior of bash, emacs, etc.)
1508 1509 """
1509 1510 document = self._control.document()
1510 1511 position -= 1
1511 1512 while position >= self._prompt_pos and \
1512 1513 not is_letter_or_number(document.characterAt(position)):
1513 1514 position -= 1
1514 1515 while position >= self._prompt_pos and \
1515 1516 is_letter_or_number(document.characterAt(position)):
1516 1517 position -= 1
1517 1518 cursor = self._control.textCursor()
1518 1519 cursor.setPosition(position + 1)
1519 1520 return cursor
1520 1521
1521 1522 def _get_word_end_cursor(self, position):
1522 1523 """ Find the end of the word to the right the given position. If a
1523 1524 sequence of non-word characters precedes the first word, skip over
1524 1525 them. (This emulates the behavior of bash, emacs, etc.)
1525 1526 """
1526 1527 document = self._control.document()
1527 1528 end = self._get_end_cursor().position()
1528 1529 while position < end and \
1529 1530 not is_letter_or_number(document.characterAt(position)):
1530 1531 position += 1
1531 1532 while position < end and \
1532 1533 is_letter_or_number(document.characterAt(position)):
1533 1534 position += 1
1534 1535 cursor = self._control.textCursor()
1535 1536 cursor.setPosition(position)
1536 1537 return cursor
1537 1538
1538 1539 def _insert_continuation_prompt(self, cursor):
1539 1540 """ Inserts new continuation prompt using the specified cursor.
1540 1541 """
1541 1542 if self._continuation_prompt_html is None:
1542 1543 self._insert_plain_text(cursor, self._continuation_prompt)
1543 1544 else:
1544 1545 self._continuation_prompt = self._insert_html_fetching_plain_text(
1545 1546 cursor, self._continuation_prompt_html)
1546 1547
1547 1548 def _insert_html(self, cursor, html):
1548 1549 """ Inserts HTML using the specified cursor in such a way that future
1549 1550 formatting is unaffected.
1550 1551 """
1551 1552 cursor.beginEditBlock()
1552 1553 cursor.insertHtml(html)
1553 1554
1554 1555 # After inserting HTML, the text document "remembers" it's in "html
1555 1556 # mode", which means that subsequent calls adding plain text will result
1556 1557 # in unwanted formatting, lost tab characters, etc. The following code
1557 1558 # hacks around this behavior, which I consider to be a bug in Qt, by
1558 1559 # (crudely) resetting the document's style state.
1559 1560 cursor.movePosition(QtGui.QTextCursor.Left,
1560 1561 QtGui.QTextCursor.KeepAnchor)
1561 1562 if cursor.selection().toPlainText() == ' ':
1562 1563 cursor.removeSelectedText()
1563 1564 else:
1564 1565 cursor.movePosition(QtGui.QTextCursor.Right)
1565 1566 cursor.insertText(' ', QtGui.QTextCharFormat())
1566 1567 cursor.endEditBlock()
1567 1568
1568 1569 def _insert_html_fetching_plain_text(self, cursor, html):
1569 1570 """ Inserts HTML using the specified cursor, then returns its plain text
1570 1571 version.
1571 1572 """
1572 1573 cursor.beginEditBlock()
1573 1574 cursor.removeSelectedText()
1574 1575
1575 1576 start = cursor.position()
1576 1577 self._insert_html(cursor, html)
1577 1578 end = cursor.position()
1578 1579 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1579 1580 text = cursor.selection().toPlainText()
1580 1581
1581 1582 cursor.setPosition(end)
1582 1583 cursor.endEditBlock()
1583 1584 return text
1584 1585
1585 1586 def _insert_plain_text(self, cursor, text):
1586 1587 """ Inserts plain text using the specified cursor, processing ANSI codes
1587 1588 if enabled.
1588 1589 """
1589 1590 cursor.beginEditBlock()
1590 1591 if self.ansi_codes:
1591 1592 for substring in self._ansi_processor.split_string(text):
1592 1593 for act in self._ansi_processor.actions:
1593 1594
1594 1595 # Unlike real terminal emulators, we don't distinguish
1595 1596 # between the screen and the scrollback buffer. A screen
1596 1597 # erase request clears everything.
1597 1598 if act.action == 'erase' and act.area == 'screen':
1598 1599 cursor.select(QtGui.QTextCursor.Document)
1599 1600 cursor.removeSelectedText()
1600 1601
1601 1602 # Simulate a form feed by scrolling just past the last line.
1602 1603 elif act.action == 'scroll' and act.unit == 'page':
1603 1604 cursor.insertText('\n')
1604 1605 cursor.endEditBlock()
1605 1606 self._set_top_cursor(cursor)
1606 1607 cursor.joinPreviousEditBlock()
1607 1608 cursor.deletePreviousChar()
1608 1609
1609 1610 elif act.action == 'carriage-return':
1610 1611 cursor.movePosition(
1611 1612 cursor.StartOfLine, cursor.KeepAnchor)
1612 1613
1613 1614 elif act.action == 'beep':
1614 1615 QtGui.qApp.beep()
1615 1616
1616 1617 elif act.action == 'backspace':
1617 1618 if not cursor.atBlockStart():
1618 1619 cursor.movePosition(
1619 1620 cursor.PreviousCharacter, cursor.KeepAnchor)
1620 1621
1621 1622 elif act.action == 'newline':
1622 1623 cursor.movePosition(cursor.EndOfLine)
1623 1624
1624 1625 format = self._ansi_processor.get_format()
1625 1626
1626 1627 selection = cursor.selectedText()
1627 1628 if len(selection) == 0:
1628 1629 cursor.insertText(substring, format)
1629 1630 elif substring is not None:
1630 1631 # BS and CR are treated as a change in print
1631 1632 # position, rather than a backwards character
1632 1633 # deletion for output equivalence with (I)Python
1633 1634 # terminal.
1634 1635 if len(substring) >= len(selection):
1635 1636 cursor.insertText(substring, format)
1636 1637 else:
1637 1638 old_text = selection[len(substring):]
1638 1639 cursor.insertText(substring + old_text, format)
1639 1640 cursor.movePosition(cursor.PreviousCharacter,
1640 1641 cursor.KeepAnchor, len(old_text))
1641 1642 else:
1642 1643 cursor.insertText(text)
1643 1644 cursor.endEditBlock()
1644 1645
1645 1646 def _insert_plain_text_into_buffer(self, cursor, text):
1646 1647 """ Inserts text into the input buffer using the specified cursor (which
1647 1648 must be in the input buffer), ensuring that continuation prompts are
1648 1649 inserted as necessary.
1649 1650 """
1650 1651 lines = text.splitlines(True)
1651 1652 if lines:
1652 1653 cursor.beginEditBlock()
1653 1654 cursor.insertText(lines[0])
1654 1655 for line in lines[1:]:
1655 1656 if self._continuation_prompt_html is None:
1656 1657 cursor.insertText(self._continuation_prompt)
1657 1658 else:
1658 1659 self._continuation_prompt = \
1659 1660 self._insert_html_fetching_plain_text(
1660 1661 cursor, self._continuation_prompt_html)
1661 1662 cursor.insertText(line)
1662 1663 cursor.endEditBlock()
1663 1664
1664 1665 def _in_buffer(self, position=None):
1665 1666 """ Returns whether the current cursor (or, if specified, a position) is
1666 1667 inside the editing region.
1667 1668 """
1668 1669 cursor = self._control.textCursor()
1669 1670 if position is None:
1670 1671 position = cursor.position()
1671 1672 else:
1672 1673 cursor.setPosition(position)
1673 1674 line = cursor.blockNumber()
1674 1675 prompt_line = self._get_prompt_cursor().blockNumber()
1675 1676 if line == prompt_line:
1676 1677 return position >= self._prompt_pos
1677 1678 elif line > prompt_line:
1678 1679 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1679 1680 prompt_pos = cursor.position() + len(self._continuation_prompt)
1680 1681 return position >= prompt_pos
1681 1682 return False
1682 1683
1683 1684 def _keep_cursor_in_buffer(self):
1684 1685 """ Ensures that the cursor is inside the editing region. Returns
1685 1686 whether the cursor was moved.
1686 1687 """
1687 1688 moved = not self._in_buffer()
1688 1689 if moved:
1689 1690 cursor = self._control.textCursor()
1690 1691 cursor.movePosition(QtGui.QTextCursor.End)
1691 1692 self._control.setTextCursor(cursor)
1692 1693 return moved
1693 1694
1694 1695 def _keyboard_quit(self):
1695 1696 """ Cancels the current editing task ala Ctrl-G in Emacs.
1696 1697 """
1697 1698 if self._temp_buffer_filled :
1698 1699 self._cancel_completion()
1699 1700 self._clear_temporary_buffer()
1700 1701 else:
1701 1702 self.input_buffer = ''
1702 1703
1703 1704 def _page(self, text, html=False):
1704 1705 """ Displays text using the pager if it exceeds the height of the
1705 1706 viewport.
1706 1707
1707 1708 Parameters:
1708 1709 -----------
1709 1710 html : bool, optional (default False)
1710 1711 If set, the text will be interpreted as HTML instead of plain text.
1711 1712 """
1712 1713 line_height = QtGui.QFontMetrics(self.font).height()
1713 1714 minlines = self._control.viewport().height() / line_height
1714 1715 if self.paging != 'none' and \
1715 1716 re.match("(?:[^\n]*\n){%i}" % minlines, text):
1716 1717 if self.paging == 'custom':
1717 1718 self.custom_page_requested.emit(text)
1718 1719 else:
1719 1720 self._page_control.clear()
1720 1721 cursor = self._page_control.textCursor()
1721 1722 if html:
1722 1723 self._insert_html(cursor, text)
1723 1724 else:
1724 1725 self._insert_plain_text(cursor, text)
1725 1726 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1726 1727
1727 1728 self._page_control.viewport().resize(self._control.size())
1728 1729 if self._splitter:
1729 1730 self._page_control.show()
1730 1731 self._page_control.setFocus()
1731 1732 else:
1732 1733 self.layout().setCurrentWidget(self._page_control)
1733 1734 elif html:
1734 1735 self._append_html(text)
1735 1736 else:
1736 1737 self._append_plain_text(text)
1737 1738
1738 1739 def _prompt_finished(self):
1739 1740 """ Called immediately after a prompt is finished, i.e. when some input
1740 1741 will be processed and a new prompt displayed.
1741 1742 """
1742 1743 self._control.setReadOnly(True)
1743 1744 self._prompt_finished_hook()
1744 1745
1745 1746 def _prompt_started(self):
1746 1747 """ Called immediately after a new prompt is displayed.
1747 1748 """
1748 1749 # Temporarily disable the maximum block count to permit undo/redo and
1749 1750 # to ensure that the prompt position does not change due to truncation.
1750 1751 self._control.document().setMaximumBlockCount(0)
1751 1752 self._control.setUndoRedoEnabled(True)
1752 1753
1753 1754 # Work around bug in QPlainTextEdit: input method is not re-enabled
1754 1755 # when read-only is disabled.
1755 1756 self._control.setReadOnly(False)
1756 1757 self._control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1757 1758
1758 1759 if not self._reading:
1759 1760 self._executing = False
1760 1761 self._prompt_started_hook()
1761 1762
1762 1763 # If the input buffer has changed while executing, load it.
1763 1764 if self._input_buffer_pending:
1764 1765 self.input_buffer = self._input_buffer_pending
1765 1766 self._input_buffer_pending = ''
1766 1767
1767 1768 self._control.moveCursor(QtGui.QTextCursor.End)
1768 1769
1769 1770 def _readline(self, prompt='', callback=None):
1770 1771 """ Reads one line of input from the user.
1771 1772
1772 1773 Parameters
1773 1774 ----------
1774 1775 prompt : str, optional
1775 1776 The prompt to print before reading the line.
1776 1777
1777 1778 callback : callable, optional
1778 1779 A callback to execute with the read line. If not specified, input is
1779 1780 read *synchronously* and this method does not return until it has
1780 1781 been read.
1781 1782
1782 1783 Returns
1783 1784 -------
1784 1785 If a callback is specified, returns nothing. Otherwise, returns the
1785 1786 input string with the trailing newline stripped.
1786 1787 """
1787 1788 if self._reading:
1788 1789 raise RuntimeError('Cannot read a line. Widget is already reading.')
1789 1790
1790 1791 if not callback and not self.isVisible():
1791 1792 # If the user cannot see the widget, this function cannot return.
1792 1793 raise RuntimeError('Cannot synchronously read a line if the widget '
1793 1794 'is not visible!')
1794 1795
1795 1796 self._reading = True
1796 1797 self._show_prompt(prompt, newline=False)
1797 1798
1798 1799 if callback is None:
1799 1800 self._reading_callback = None
1800 1801 while self._reading:
1801 1802 QtCore.QCoreApplication.processEvents()
1802 1803 return self._get_input_buffer(force=True).rstrip('\n')
1803 1804
1804 1805 else:
1805 1806 self._reading_callback = lambda: \
1806 1807 callback(self._get_input_buffer(force=True).rstrip('\n'))
1807 1808
1808 1809 def _set_continuation_prompt(self, prompt, html=False):
1809 1810 """ Sets the continuation prompt.
1810 1811
1811 1812 Parameters
1812 1813 ----------
1813 1814 prompt : str
1814 1815 The prompt to show when more input is needed.
1815 1816
1816 1817 html : bool, optional (default False)
1817 1818 If set, the prompt will be inserted as formatted HTML. Otherwise,
1818 1819 the prompt will be treated as plain text, though ANSI color codes
1819 1820 will be handled.
1820 1821 """
1821 1822 if html:
1822 1823 self._continuation_prompt_html = prompt
1823 1824 else:
1824 1825 self._continuation_prompt = prompt
1825 1826 self._continuation_prompt_html = None
1826 1827
1827 1828 def _set_cursor(self, cursor):
1828 1829 """ Convenience method to set the current cursor.
1829 1830 """
1830 1831 self._control.setTextCursor(cursor)
1831 1832
1832 1833 def _set_top_cursor(self, cursor):
1833 1834 """ Scrolls the viewport so that the specified cursor is at the top.
1834 1835 """
1835 1836 scrollbar = self._control.verticalScrollBar()
1836 1837 scrollbar.setValue(scrollbar.maximum())
1837 1838 original_cursor = self._control.textCursor()
1838 1839 self._control.setTextCursor(cursor)
1839 1840 self._control.ensureCursorVisible()
1840 1841 self._control.setTextCursor(original_cursor)
1841 1842
1842 1843 def _show_prompt(self, prompt=None, html=False, newline=True):
1843 1844 """ Writes a new prompt at the end of the buffer.
1844 1845
1845 1846 Parameters
1846 1847 ----------
1847 1848 prompt : str, optional
1848 1849 The prompt to show. If not specified, the previous prompt is used.
1849 1850
1850 1851 html : bool, optional (default False)
1851 1852 Only relevant when a prompt is specified. If set, the prompt will
1852 1853 be inserted as formatted HTML. Otherwise, the prompt will be treated
1853 1854 as plain text, though ANSI color codes will be handled.
1854 1855
1855 1856 newline : bool, optional (default True)
1856 1857 If set, a new line will be written before showing the prompt if
1857 1858 there is not already a newline at the end of the buffer.
1858 1859 """
1859 1860 # Save the current end position to support _append*(before_prompt=True).
1860 1861 cursor = self._get_end_cursor()
1861 1862 self._append_before_prompt_pos = cursor.position()
1862 1863
1863 1864 # Insert a preliminary newline, if necessary.
1864 1865 if newline and cursor.position() > 0:
1865 1866 cursor.movePosition(QtGui.QTextCursor.Left,
1866 1867 QtGui.QTextCursor.KeepAnchor)
1867 1868 if cursor.selection().toPlainText() != '\n':
1868 1869 self._append_plain_text('\n')
1869 1870
1870 1871 # Write the prompt.
1871 1872 self._append_plain_text(self._prompt_sep)
1872 1873 if prompt is None:
1873 1874 if self._prompt_html is None:
1874 1875 self._append_plain_text(self._prompt)
1875 1876 else:
1876 1877 self._append_html(self._prompt_html)
1877 1878 else:
1878 1879 if html:
1879 1880 self._prompt = self._append_html_fetching_plain_text(prompt)
1880 1881 self._prompt_html = prompt
1881 1882 else:
1882 1883 self._append_plain_text(prompt)
1883 1884 self._prompt = prompt
1884 1885 self._prompt_html = None
1885 1886
1886 1887 self._prompt_pos = self._get_end_cursor().position()
1887 1888 self._prompt_started()
1888 1889
1889 1890 #------ Signal handlers ----------------------------------------------------
1890 1891
1891 1892 def _adjust_scrollbars(self):
1892 1893 """ Expands the vertical scrollbar beyond the range set by Qt.
1893 1894 """
1894 1895 # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
1895 1896 # and qtextedit.cpp.
1896 1897 document = self._control.document()
1897 1898 scrollbar = self._control.verticalScrollBar()
1898 1899 viewport_height = self._control.viewport().height()
1899 1900 if isinstance(self._control, QtGui.QPlainTextEdit):
1900 1901 maximum = max(0, document.lineCount() - 1)
1901 1902 step = viewport_height / self._control.fontMetrics().lineSpacing()
1902 1903 else:
1903 1904 # QTextEdit does not do line-based layout and blocks will not in
1904 1905 # general have the same height. Therefore it does not make sense to
1905 1906 # attempt to scroll in line height increments.
1906 1907 maximum = document.size().height()
1907 1908 step = viewport_height
1908 1909 diff = maximum - scrollbar.maximum()
1909 1910 scrollbar.setRange(0, maximum)
1910 1911 scrollbar.setPageStep(step)
1911 1912
1912 1913 # Compensate for undesirable scrolling that occurs automatically due to
1913 1914 # maximumBlockCount() text truncation.
1914 1915 if diff < 0 and document.blockCount() == document.maximumBlockCount():
1915 1916 scrollbar.setValue(scrollbar.value() + diff)
1916 1917
1917 1918 def _custom_context_menu_requested(self, pos):
1918 1919 """ Shows a context menu at the given QPoint (in widget coordinates).
1919 1920 """
1920 1921 menu = self._context_menu_make(pos)
1921 1922 menu.exec_(self._control.mapToGlobal(pos))
General Comments 0
You need to be logged in to leave comments. Login now