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