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