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